From f4ea016a95655bd1aee1a95fc2e2b9a9184f6ae6 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Mon, 13 Jul 2026 14:28:49 +0200 Subject: [PATCH 1/2] feat: add new assertions for July 2026 Signed-off-by: Kenny Pflug --- Light.GuardClauses.SingleFile.cs | 912 +++++++++++++++++- ai-plans/0145-additional-assertions.md | 72 ++ .../CommonAssertions/UuidVersion7Benchmark.cs | 49 + .../Light.GuardClauses.Performance.csproj | 2 +- .../Light.GuardClauses.Performance/Program.cs | 40 +- docs/assertion-overview.md | 14 +- docs/contributing-and-building.md | 2 +- docs/historical-performance.md | 2 +- docs/source-code-inclusion.md | 2 +- src/Light.GuardClauses/Check.IsAscii.cs | 84 ++ src/Light.GuardClauses/Check.IsFinite.cs | 30 + .../Check.IsUuidVersion7.cs | 20 + src/Light.GuardClauses/Check.MustBeAscii.cs | 302 ++++++ src/Light.GuardClauses/Check.MustBeFinite.cs | 116 +++ src/Light.GuardClauses/Check.MustBeUtc.cs | 45 + .../Check.MustBeUuidVersion7.cs | 54 ++ .../Check.MustHaveCountIn.cs | 65 ++ .../Check.MustHaveLength.cs | 58 ++ .../Check.MustHaveLengthIn.cs | 124 +++ .../Check.MustNotBeEmpty.cs | 116 +++ .../Check.MustNotBeEmptyOrWhiteSpace.cs | 132 +++ .../Throw.CollectionCountNotInRange.cs | 28 + .../ExceptionFactory/Throw.DateTime.cs | 17 + .../ExceptionFactory/Throw.NotFinite.cs | 24 + .../ExceptionFactory/Throw.Span.cs | 17 + .../Throw.WhiteSpaceString.cs | 18 + .../Exceptions/InvalidDateTimeException.cs | 4 +- src/Light.GuardClauses/GuidLayout.cs | 22 + .../SourceFileMergerFrameworkTests.cs | 4 + .../SourceFileMergerWhitelistTests.cs | 60 +- .../AdditionalSpanAndMemoryGuardsTests.cs | 167 ++++ .../MustHaveCountInTests.cs | 138 +++ .../CommonAssertions/FiniteTests.cs | 86 ++ .../CommonAssertions/UuidVersion7Tests.cs | 48 + .../DateTimeAssertions/MustBeUtcTests.cs | 38 +- .../StringAssertions/AsciiTests.cs | 157 +++ .../AssertionWhitelist.cs | 16 + .../SourceFileMerger.cs | 1 + .../settings.json | 8 + 39 files changed, 3035 insertions(+), 59 deletions(-) create mode 100644 ai-plans/0145-additional-assertions.md create mode 100644 benchmarks/Light.GuardClauses.Performance/CommonAssertions/UuidVersion7Benchmark.cs create mode 100644 src/Light.GuardClauses/Check.IsAscii.cs create mode 100644 src/Light.GuardClauses/Check.IsFinite.cs create mode 100644 src/Light.GuardClauses/Check.IsUuidVersion7.cs create mode 100644 src/Light.GuardClauses/Check.MustBeAscii.cs create mode 100644 src/Light.GuardClauses/Check.MustBeFinite.cs create mode 100644 src/Light.GuardClauses/Check.MustBeUuidVersion7.cs create mode 100644 src/Light.GuardClauses/Check.MustHaveCountIn.cs create mode 100644 src/Light.GuardClauses/Check.MustNotBeEmptyOrWhiteSpace.cs create mode 100644 src/Light.GuardClauses/ExceptionFactory/Throw.CollectionCountNotInRange.cs create mode 100644 src/Light.GuardClauses/ExceptionFactory/Throw.NotFinite.cs create mode 100644 src/Light.GuardClauses/GuidLayout.cs create mode 100644 tests/Light.GuardClauses.Tests/CollectionAssertions/AdditionalSpanAndMemoryGuardsTests.cs create mode 100644 tests/Light.GuardClauses.Tests/CollectionAssertions/MustHaveCountInTests.cs create mode 100644 tests/Light.GuardClauses.Tests/CommonAssertions/FiniteTests.cs create mode 100644 tests/Light.GuardClauses.Tests/CommonAssertions/UuidVersion7Tests.cs create mode 100644 tests/Light.GuardClauses.Tests/StringAssertions/AsciiTests.cs diff --git a/Light.GuardClauses.SingleFile.cs b/Light.GuardClauses.SingleFile.cs index 192bbe6a..cda3893c 100644 --- a/Light.GuardClauses.SingleFile.cs +++ b/Light.GuardClauses.SingleFile.cs @@ -35,6 +35,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Text; using System.Text.RegularExpressions; @@ -507,6 +508,64 @@ public static T MustNotBeIn([NotNull][ValidatedNotNull] this T parameter, Ran /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsTrimmedAtEnd(this ReadOnlySpan parameter) => parameter.Length == 0 || !parameter[parameter.Length - 1].IsWhiteSpace(); + /// + /// Ensures that the specified single-precision floating-point value is finite, or otherwise throws an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MustBeFinite(this float parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + if (!parameter.IsFinite()) + { + Throw.NotFinite(parameter, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified single-precision floating-point value is finite, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static float MustBeFinite(this float parameter, Func exceptionFactory) + { + if (!parameter.IsFinite()) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the specified double-precision floating-point value is finite, or otherwise throws an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double MustBeFinite(this double parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + if (!parameter.IsFinite()) + { + Throw.NotFinite(parameter, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified double-precision floating-point value is finite, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static double MustBeFinite(this double parameter, Func exceptionFactory) + { + if (!parameter.IsFinite()) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + /// /// Ensures that the has at most the specified length, or otherwise throws an . /// @@ -1074,6 +1133,239 @@ public static T MustBeLessThanOrEqualTo([NotNull][ValidatedNotNull] this T pa return parameter; } + /// Ensures that the character is ASCII, or otherwise throws an . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static char MustBeAscii(this char parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + if (!parameter.IsAscii()) + { + Throw.Argument(parameterName, message ?? $"{parameterName ?? "The character"} must be ASCII, but it actually is '{parameter}'."); + } + + return parameter; + } + + /// Ensures that the character is ASCII, or otherwise throws your custom exception. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static char MustBeAscii(this char parameter, Func exceptionFactory) + { + if (!parameter.IsAscii()) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// Ensures that the byte is ASCII, or otherwise throws an . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte MustBeAscii(this byte parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + if (!parameter.IsAscii()) + { + Throw.Argument(parameterName, message ?? $"{parameterName ?? "The byte"} must be ASCII, but it actually is {parameter}."); + } + + return parameter; + } + + /// Ensures that the byte is ASCII, or otherwise throws your custom exception. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static byte MustBeAscii(this byte parameter, Func exceptionFactory) + { + if (!parameter.IsAscii()) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// Ensures that the string is non-null and contains only ASCII characters. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static string MustBeAscii([NotNull][ValidatedNotNull] this string? parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + parameter.MustNotBeNull(parameterName, message); + if (!parameter.IsAscii()) + { + Throw.Argument(parameterName, message ?? $"{parameterName ?? "The string"} must contain only ASCII characters."); + } + + return parameter; + } + + /// Ensures that the string is non-null and contains only ASCII characters, or otherwise throws your custom exception. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull; exceptionFactory:null => halt")] + public static string MustBeAscii([NotNull][ValidatedNotNull] this string? parameter, Func exceptionFactory) + { + if (parameter is null || !parameter.IsAscii()) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// Ensures that the character span contains only ASCII characters. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeAscii(this Span parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + ((ReadOnlySpan)parameter).MustBeAscii(parameterName, message); + return parameter; + } + + /// Ensures that the character span contains only ASCII characters, or otherwise throws your custom exception. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeAscii(this Span parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + ((ReadOnlySpan)parameter).MustBeAscii(exceptionFactory); + return parameter; + } + + /// Ensures that the read-only character span contains only ASCII characters. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan MustBeAscii(this ReadOnlySpan parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + if (!parameter.IsAscii()) + { + Throw.Argument(parameterName, message ?? $"{parameterName ?? "The character span"} must contain only ASCII characters."); + } + + return parameter; + } + + /// Ensures that the read-only character span contains only ASCII characters, or otherwise throws your custom exception. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan MustBeAscii(this ReadOnlySpan parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + if (!parameter.IsAscii()) + { + Throw.CustomSpanException(exceptionFactory, parameter); + } + + return parameter; + } + + /// Ensures that the character memory contains only ASCII characters. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeAscii(this Memory parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + ((ReadOnlySpan)parameter.Span).MustBeAscii(parameterName, message); + return parameter; + } + + /// Ensures that the character memory contains only ASCII characters, or otherwise throws your custom exception. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeAscii(this Memory parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + ((ReadOnlySpan)parameter.Span).MustBeAscii(exceptionFactory); + return parameter; + } + + /// Ensures that the read-only character memory contains only ASCII characters. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeAscii(this ReadOnlyMemory parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + parameter.Span.MustBeAscii(parameterName, message); + return parameter; + } + + /// Ensures that the read-only character memory contains only ASCII characters, or otherwise throws your custom exception. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeAscii(this ReadOnlyMemory parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + parameter.Span.MustBeAscii(exceptionFactory); + return parameter; + } + + /// Ensures that the byte span contains only ASCII values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeAscii(this Span parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + ((ReadOnlySpan)parameter).MustBeAscii(parameterName, message); + return parameter; + } + + /// Ensures that the byte span contains only ASCII values, or otherwise throws your custom exception. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeAscii(this Span parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + ((ReadOnlySpan)parameter).MustBeAscii(exceptionFactory); + return parameter; + } + + /// Ensures that the read-only byte span contains only ASCII values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan MustBeAscii(this ReadOnlySpan parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + if (!parameter.IsAscii()) + { + Throw.Argument(parameterName, message ?? $"{parameterName ?? "The byte span"} must contain only ASCII values."); + } + + return parameter; + } + + /// Ensures that the read-only byte span contains only ASCII values, or otherwise throws your custom exception. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan MustBeAscii(this ReadOnlySpan parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + if (!parameter.IsAscii()) + { + Throw.CustomSpanException(exceptionFactory, parameter); + } + + return parameter; + } + + /// Ensures that the byte memory contains only ASCII values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeAscii(this Memory parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + ((ReadOnlySpan)parameter.Span).MustBeAscii(parameterName, message); + return parameter; + } + + /// Ensures that the byte memory contains only ASCII values, or otherwise throws your custom exception. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeAscii(this Memory parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + ((ReadOnlySpan)parameter.Span).MustBeAscii(exceptionFactory); + return parameter; + } + + /// Ensures that the read-only byte memory contains only ASCII values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeAscii(this ReadOnlyMemory parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + parameter.Span.MustBeAscii(parameterName, message); + return parameter; + } + + /// Ensures that the read-only byte memory contains only ASCII values, or otherwise throws your custom exception. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeAscii(this ReadOnlyMemory parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + parameter.Span.MustBeAscii(exceptionFactory); + return parameter; + } + + /// + /// Checks if the specified GUID structurally identifies an RFC/IETF UUID version 7. + /// + /// The GUID to be checked. + /// True when the UUID version is 7 and the variant is RFC/IETF, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsUuidVersion7(this Guid parameter) + { + var layout = new GuidLayout(parameter); + return (layout.TimeHighAndVersion & 0xF000) == 0x7000 && (layout.ClockSequenceHighAndReserved & 0xC0) == 0x80; + } + /// /// Ensures that the string is not null and trimmed at the end, or otherwise throws a . /// Empty strings are regarded as trimmed. @@ -2291,6 +2583,94 @@ public static ImmutableArray MustHaveLengthIn(this ImmutableArray param return parameter; } + /// + /// Ensures that the span length is within the specified range, or otherwise throws an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustHaveLengthIn(this Span parameter, Range range, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + ((ReadOnlySpan)parameter).MustHaveLengthIn(range, parameterName, message); + return parameter; + } + + /// + /// Ensures that the span length is within the specified range, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustHaveLengthIn(this Span parameter, Range range, ReadOnlySpanExceptionFactory> exceptionFactory) + { + ((ReadOnlySpan)parameter).MustHaveLengthIn(range, exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only span length is within the specified range, or otherwise throws an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan MustHaveLengthIn(this ReadOnlySpan parameter, Range range, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + if (!range.IsValueWithinRange(parameter.Length)) + { + Throw.SpanLengthNotInRange(parameter, range, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the read-only span length is within the specified range, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan MustHaveLengthIn(this ReadOnlySpan parameter, Range range, ReadOnlySpanExceptionFactory> exceptionFactory) + { + if (!range.IsValueWithinRange(parameter.Length)) + { + Throw.CustomSpanException(exceptionFactory, parameter, range); + } + + return parameter; + } + + /// + /// Ensures that the memory length is within the specified range, or otherwise throws an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustHaveLengthIn(this Memory parameter, Range range, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + ((ReadOnlySpan)parameter.Span).MustHaveLengthIn(range, parameterName, message); + return parameter; + } + + /// + /// Ensures that the memory length is within the specified range, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustHaveLengthIn(this Memory parameter, Range range, ReadOnlySpanExceptionFactory> exceptionFactory) + { + ((ReadOnlySpan)parameter.Span).MustHaveLengthIn(range, exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only memory length is within the specified range, or otherwise throws an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustHaveLengthIn(this ReadOnlyMemory parameter, Range range, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + parameter.Span.MustHaveLengthIn(range, parameterName, message); + return parameter; + } + + /// + /// Ensures that the read-only memory length is within the specified range, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustHaveLengthIn(this ReadOnlyMemory parameter, Range range, ReadOnlySpanExceptionFactory> exceptionFactory) + { + parameter.Span.MustHaveLengthIn(range, exceptionFactory); + return parameter; + } + /// /// Checks if and point to the same object. /// @@ -2644,6 +3024,51 @@ public static string MustBeSubstringOf([NotNull][ValidatedNotNull] this string? return parameter; } + /// + /// Ensures that the collection count is within the specified range, or otherwise throws an . + /// + /// The collection to be checked. + /// The range in which the collection count must lie. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The original collection. + /// Thrown when the collection count is not within . + /// Thrown when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static TCollection MustHaveCountIn([NotNull][ValidatedNotNull] this TCollection? parameter, Range range, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + where TCollection : class, IEnumerable + { + var actualCount = parameter.Count(parameterName, message); + if (!range.IsValueWithinRange(actualCount)) + { + Throw.CollectionCountNotInRange(parameter, actualCount, range, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the collection count is within the specified range, or otherwise throws your custom exception. + /// + /// The collection to be checked. + /// The range in which the collection count must lie. + /// The delegate that creates your custom exception. and are passed to this delegate. + /// The original collection. + /// Your custom exception thrown when the collection is null or its count is not within . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull; exceptionFactory:null => halt")] + public static TCollection MustHaveCountIn([NotNull][ValidatedNotNull] this TCollection? parameter, Range range, Func, Exception> exceptionFactory) + where TCollection : class, IEnumerable + { + if (parameter is null || !range.IsValueWithinRange(parameter.Count())) + { + Throw.CustomException(exceptionFactory, parameter, range); + } + + return parameter; + } + /// /// Checks if the string is either "\n" or "\r\n". This is done independently of the current value of . /// @@ -2720,77 +3145,222 @@ public static T MustBeValidEnumValue(this T parameter, [CallerArgumentExpress { if (!EnumInfo.IsValidEnumValue(parameter)) { - Throw.EnumValueNotDefined(parameter, parameterName, message); + Throw.EnumValueNotDefined(parameter, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified enum value is valid, or otherwise throws your custom exception. An enum value + /// is valid when the specified value is one of the constants defined in the enum, or a valid flags combination when the enum type + /// is marked with the . + /// + /// The type of the enum. + /// The value to be checked. + /// The delegate that creates your custom exception. The is passed to this delegate. + /// Your custom exception thrown when is no valid enum value, or when is no enum type. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static T MustBeValidEnumValue(this T parameter, Func exceptionFactory) + where T : struct, Enum + { + if (!EnumInfo.IsValidEnumValue(parameter)) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Checks if the specified string is null, empty, or contains only white space. + /// + /// The string to be checked. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("=> false, string:notnull; => true, string:canbenull")] + public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? @string) => string.IsNullOrWhiteSpace(@string); + /// + /// Ensures that the specified GUID is not empty, or otherwise throws an . + /// + /// The GUID to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when is an empty GUID. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Guid MustNotBeEmpty(this Guid parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + if (parameter == Guid.Empty) + { + Throw.EmptyGuid(parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified GUID is not empty, or otherwise throws your custom exception. + /// + /// The GUID to be checked. + /// The delegate that creates your custom exception. + /// Your custom exception thrown when is an empty GUID. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static Guid MustNotBeEmpty(this Guid parameter, Func exceptionFactory) + { + if (parameter == Guid.Empty) + { + Throw.CustomException(exceptionFactory); + } + + return parameter; + } + + /// + /// Ensures that the specified span is not empty, or otherwise throws an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustNotBeEmpty(this Span parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + ((ReadOnlySpan)parameter).MustNotBeEmpty(parameterName, message); + return parameter; + } + + /// + /// Ensures that the specified span is not empty, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustNotBeEmpty(this Span parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + ((ReadOnlySpan)parameter).MustNotBeEmpty(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the specified read-only span is not empty, or otherwise throws an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan MustNotBeEmpty(this ReadOnlySpan parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + if (parameter.IsEmpty) + { + Throw.EmptyCollection(parameterName, message); } return parameter; } /// - /// Ensures that the specified enum value is valid, or otherwise throws your custom exception. An enum value - /// is valid when the specified value is one of the constants defined in the enum, or a valid flags combination when the enum type - /// is marked with the . + /// Ensures that the specified read-only span is not empty, or otherwise throws your custom exception. /// - /// The type of the enum. - /// The value to be checked. - /// The delegate that creates your custom exception. The is passed to this delegate. - /// Your custom exception thrown when is no valid enum value, or when is no enum type. [MethodImpl(MethodImplOptions.AggressiveInlining)] - [ContractAnnotation("exceptionFactory:null => halt")] - public static T MustBeValidEnumValue(this T parameter, Func exceptionFactory) - where T : struct, Enum + public static ReadOnlySpan MustNotBeEmpty(this ReadOnlySpan parameter, ReadOnlySpanExceptionFactory exceptionFactory) { - if (!EnumInfo.IsValidEnumValue(parameter)) + if (parameter.IsEmpty) { - Throw.CustomException(exceptionFactory, parameter); + Throw.CustomSpanException(exceptionFactory, parameter); } return parameter; } /// - /// Checks if the specified string is null, empty, or contains only white space. + /// Ensures that the specified memory is not empty, or otherwise throws an . /// - /// The string to be checked. [MethodImpl(MethodImplOptions.AggressiveInlining)] - [ContractAnnotation("=> false, string:notnull; => true, string:canbenull")] - public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? @string) => string.IsNullOrWhiteSpace(@string); + public static Memory MustNotBeEmpty(this Memory parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + ((ReadOnlySpan)parameter.Span).MustNotBeEmpty(parameterName, message); + return parameter; + } + /// - /// Ensures that the specified GUID is not empty, or otherwise throws an . + /// Ensures that the specified memory is not empty, or otherwise throws your custom exception. /// - /// The GUID to be checked. - /// The name of the parameter (optional). - /// The message that will be passed to the resulting exception (optional). - /// Thrown when is an empty GUID. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Guid MustNotBeEmpty(this Guid parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + public static Memory MustNotBeEmpty(this Memory parameter, ReadOnlySpanExceptionFactory exceptionFactory) { - if (parameter == Guid.Empty) - { - Throw.EmptyGuid(parameterName, message); - } + ((ReadOnlySpan)parameter.Span).MustNotBeEmpty(exceptionFactory); + return parameter; + } + /// + /// Ensures that the specified read-only memory is not empty, or otherwise throws an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustNotBeEmpty(this ReadOnlyMemory parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + parameter.Span.MustNotBeEmpty(parameterName, message); return parameter; } /// - /// Ensures that the specified GUID is not empty, or otherwise throws your custom exception. + /// Ensures that the specified read-only memory is not empty, or otherwise throws your custom exception. /// - /// The GUID to be checked. - /// The delegate that creates your custom exception. - /// Your custom exception thrown when is an empty GUID. [MethodImpl(MethodImplOptions.AggressiveInlining)] - [ContractAnnotation("exceptionFactory:null => halt")] - public static Guid MustNotBeEmpty(this Guid parameter, Func exceptionFactory) + public static ReadOnlyMemory MustNotBeEmpty(this ReadOnlyMemory parameter, ReadOnlySpanExceptionFactory exceptionFactory) { - if (parameter == Guid.Empty) + parameter.Span.MustNotBeEmpty(exceptionFactory); + return parameter; + } + + /// Checks if the character is an ASCII code point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAscii(this char parameter) => parameter <= 0x7F; + /// Checks if the byte is an ASCII value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAscii(this byte parameter) => parameter <= 0x7F; + /// Checks if the string is non-null and contains only ASCII characters. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAscii(this string? parameter) => parameter is not null && parameter.AsSpan().IsAscii(); + /// Checks if the character span contains only ASCII characters. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAscii(this Span parameter) => ((ReadOnlySpan)parameter).IsAscii(); + /// Checks if the read-only character span contains only ASCII characters. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAscii(this ReadOnlySpan parameter) + { + foreach (var value in parameter) { - Throw.CustomException(exceptionFactory); + if (value > 0x7F) + { + return false; + } } - return parameter; + return true; + } + + /// Checks if the character memory contains only ASCII characters. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAscii(this Memory parameter) => parameter.Span.IsAscii(); + /// Checks if the read-only character memory contains only ASCII characters. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAscii(this ReadOnlyMemory parameter) => parameter.Span.IsAscii(); + /// Checks if the byte span contains only ASCII values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAscii(this Span parameter) => ((ReadOnlySpan)parameter).IsAscii(); + /// Checks if the read-only byte span contains only ASCII values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAscii(this ReadOnlySpan parameter) + { + foreach (var value in parameter) + { + if (value > 0x7F) + { + return false; + } + } + + return true; } + /// Checks if the byte memory contains only ASCII values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAscii(this Memory parameter) => parameter.Span.IsAscii(); + /// Checks if the read-only byte memory contains only ASCII values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAscii(this ReadOnlyMemory parameter) => parameter.Span.IsAscii(); /// /// Ensures that the string is either "\n" or "\r\n", or otherwise throws a . This is done independently of the current value of . /// @@ -3377,12 +3947,155 @@ public static ImmutableArray MustHaveMinimumLength(this ImmutableArray return parameter; } + /// + /// Checks if the specified single-precision floating-point value is finite. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsFinite(this float parameter) => parameter >= float.MinValue && parameter <= float.MaxValue; + /// + /// Checks if the specified double-precision floating-point value is finite. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsFinite(this double parameter) => parameter >= double.MinValue && parameter <= double.MaxValue; + /// + /// Ensures that the character span is neither empty nor all white space. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustNotBeEmptyOrWhiteSpace(this Span parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + ((ReadOnlySpan)parameter).MustNotBeEmptyOrWhiteSpace(parameterName, message); + return parameter; + } + + /// + /// Ensures that the character span is neither empty nor all white space, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustNotBeEmptyOrWhiteSpace(this Span parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + ((ReadOnlySpan)parameter).MustNotBeEmptyOrWhiteSpace(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only character span is neither empty nor all white space. + /// + /// Thrown when is empty. + /// Thrown when contains only white space. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan MustNotBeEmptyOrWhiteSpace(this ReadOnlySpan parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + if (parameter.IsEmpty) + { + Throw.EmptyString(parameterName, message); + } + + if (parameter.IsEmptyOrWhiteSpace()) + { + Throw.WhiteSpaceSpan(parameter, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the read-only character span is neither empty nor all white space, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan MustNotBeEmptyOrWhiteSpace(this ReadOnlySpan parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + if (parameter.IsEmptyOrWhiteSpace()) + { + Throw.CustomSpanException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the character memory is neither empty nor all white space. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustNotBeEmptyOrWhiteSpace(this Memory parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + ((ReadOnlySpan)parameter.Span).MustNotBeEmptyOrWhiteSpace(parameterName, message); + return parameter; + } + + /// + /// Ensures that the character memory is neither empty nor all white space, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustNotBeEmptyOrWhiteSpace(this Memory parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + ((ReadOnlySpan)parameter.Span).MustNotBeEmptyOrWhiteSpace(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only character memory is neither empty nor all white space. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustNotBeEmptyOrWhiteSpace(this ReadOnlyMemory parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + parameter.Span.MustNotBeEmptyOrWhiteSpace(parameterName, message); + return parameter; + } + + /// + /// Ensures that the read-only character memory is neither empty nor all white space, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustNotBeEmptyOrWhiteSpace(this ReadOnlyMemory parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + parameter.Span.MustNotBeEmptyOrWhiteSpace(exceptionFactory); + return parameter; + } + /// /// Checks if the specified GUID is an empty one. /// /// The GUID to be checked. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsEmpty(this Guid parameter) => parameter == Guid.Empty; + /// + /// Ensures that the specified GUID structurally identifies an RFC/IETF UUID version 7, or otherwise throws an . + /// + /// The GUID to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The original GUID. + /// Thrown when the UUID version is not 7 or the variant is not RFC/IETF. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Guid MustBeUuidVersion7(this Guid parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + if (!parameter.IsUuidVersion7()) + { + Throw.Argument(parameterName, message ?? $"{parameterName ?? "The GUID"} must be an RFC/IETF UUID version 7, but it actually is \"{parameter}\"."); + } + + return parameter; + } + + /// + /// Ensures that the specified GUID structurally identifies an RFC/IETF UUID version 7, or otherwise throws your custom exception. + /// + /// The GUID to be checked. + /// The delegate that creates your custom exception. is passed to this delegate. + /// The original GUID. + /// Your custom exception thrown when the UUID version is not 7 or the variant is not RFC/IETF. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static Guid MustBeUuidVersion7(this Guid parameter, Func exceptionFactory) + { + if (!parameter.IsUuidVersion7()) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + /// /// Ensures that the specified object reference is not null, or otherwise throws an . /// @@ -4538,6 +5251,46 @@ public static ReadOnlySpan MustHaveLength(this ReadOnlySpan parameter, return parameter; } + /// + /// Ensures that the memory has the specified length, or otherwise throws an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustHaveLength(this Memory parameter, int length, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + ((ReadOnlySpan)parameter.Span).MustHaveLength(length, parameterName, message); + return parameter; + } + + /// + /// Ensures that the memory has the specified length, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustHaveLength(this Memory parameter, int length, ReadOnlySpanExceptionFactory exceptionFactory) + { + parameter.Span.MustHaveLength(length, exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only memory has the specified length, or otherwise throws an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustHaveLength(this ReadOnlyMemory parameter, int length, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + parameter.Span.MustHaveLength(length, parameterName, message); + return parameter; + } + + /// + /// Ensures that the read-only memory has the specified length, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustHaveLength(this ReadOnlyMemory parameter, int length, ReadOnlySpanExceptionFactory exceptionFactory) + { + parameter.Span.MustHaveLength(length, exceptionFactory); + return parameter; + } + /// /// Ensures that the span has the specified length, or otherwise throws your custom exception. /// @@ -4997,6 +5750,44 @@ public static DateTime MustBeUtc(this DateTime parameter, Func + /// Ensures that the specified has a zero offset, or otherwise throws an . + /// + /// The date time offset to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The original date time offset without conversion. + /// Thrown when does not have as its offset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static DateTimeOffset MustBeUtc(this DateTimeOffset parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + if (parameter.Offset != TimeSpan.Zero) + { + Throw.MustBeUtcDateTimeOffset(parameter, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified has a zero offset, or otherwise throws your custom exception. + /// + /// The date time offset to be checked. + /// The delegate that creates your custom exception. is passed to this delegate. + /// The original date time offset without conversion. + /// Your custom exception thrown when does not have as its offset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static DateTimeOffset MustBeUtc(this DateTimeOffset parameter, Func exceptionFactory) + { + if (parameter.Offset != TimeSpan.Zero) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + /// /// Checks if the specified character is a digit. /// @@ -6037,6 +6828,21 @@ public int GetHashCode(string @string) } } + /// + /// Overlays the stable sequential GUID field layout so UUID structural fields can be inspected without allocations. + /// + [StructLayout(LayoutKind.Explicit, Size = 16)] + internal readonly struct GuidLayout + { + [FieldOffset(0)] + private readonly Guid _value; + [FieldOffset(6)] + public readonly ushort TimeHighAndVersion; + [FieldOffset(8)] + public readonly byte ClockSequenceHighAndReserved; + public GuidLayout(Guid value) => _value = value; + } + /// /// Represents an that uses /// to compare types. This check works like the normal type equality comparison, but when two @@ -6206,7 +7012,7 @@ protected InvalidUriSchemeException(SerializationInfo info, StreamingContext con } /// - /// This exception indicates that a value is invalid. + /// This exception indicates that a or value is invalid. /// [Serializable] internal class InvalidDateTimeException : ArgumentException @@ -6854,6 +7660,18 @@ public static void MustBeLessThan(T parameter, T boundary, [CallerArgumentExp public static void MustBeGreaterThanOrEqualTo(T parameter, T boundary, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) where T : IComparable => throw new ArgumentOutOfRangeException(parameterName, message ?? $"{parameterName ?? "The value"} must be greater than or equal to {boundary}, but it actually is {parameter}."); /// + /// Throws an indicating that a floating-point value is not finite. + /// + [ContractAnnotation("=> halt")] + [DoesNotReturn] + public static void NotFinite(T parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) => throw new ArgumentOutOfRangeException(parameterName, message ?? $"{parameterName ?? "The value"} must be finite, but it actually is {parameter}."); + /// + /// Throws the default indicating that a collection count is outside a range. + /// + [ContractAnnotation("=> halt")] + [DoesNotReturn] + public static void CollectionCountNotInRange(IEnumerable parameter, int actualCount, Range range, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) => throw new InvalidCollectionCountException(parameterName, message ?? $"{parameterName ?? "The collection"} must have its count between {range.CreateRangeDescriptionText("and")}, but it actually has count {actualCount}."); + /// /// Throws the default indicating that a string is not equal to "\n" or "\r\n". /// [ContractAnnotation("=> halt")] @@ -6897,6 +7715,13 @@ public static void MustNotBeInRange(T parameter, Range range, [CallerArgum [DoesNotReturn] public static void WhiteSpaceString(string parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) => throw new WhiteSpaceStringException(parameterName, message ?? $"{parameterName ?? "The string"} must not contain only white space, but it actually is \"{parameter}\"."); /// + /// Throws the default indicating that a character span contains only + /// white space, using the optional parameter name and message. + /// + [ContractAnnotation("=> halt")] + [DoesNotReturn] + public static void WhiteSpaceSpan(ReadOnlySpan parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) => throw new WhiteSpaceStringException(parameterName, message ?? $"{parameterName ?? "The character span"} must not contain only white space, but it actually has length {parameter.Length}."); + /// /// Throws an using the optional message. /// [ContractAnnotation("=> halt")] @@ -6975,6 +7800,13 @@ public static void SameObjectReference(T? parameter, [CallerArgumentExpressio [DoesNotReturn] public static void MustBeUtcDateTime(DateTime parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) => throw new InvalidDateTimeException(parameterName, message ?? $"{parameterName ?? "The date time"} must use kind \"{DateTimeKind.Utc}\", but it actually uses \"{parameter.Kind}\" and is \"{parameter:O}\"."); /// + /// Throws the default indicating that a date time offset does not use + /// as its offset, using the optional parameter name and message. + /// + [ContractAnnotation("=> halt")] + [DoesNotReturn] + public static void MustBeUtcDateTimeOffset(DateTimeOffset parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) => throw new InvalidDateTimeException(parameterName, message ?? $"{parameterName ?? "The date time offset"} must use offset \"{TimeSpan.Zero}\", but it actually uses \"{parameter.Offset}\" and is \"{parameter:O}\"."); + /// /// Throws the default indicating that a date time is not using /// , using the optional parameter name and message. /// @@ -7212,6 +8044,12 @@ public static void InvalidMinimumImmutableArrayLength(ImmutableArray param [DoesNotReturn] public static void InvalidSpanLength(ReadOnlySpan parameter, int length, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) => throw new InvalidCollectionCountException(parameterName, message ?? $"{parameterName ?? "The read-only span"} must have length {length}, but it actually has length {parameter.Length}."); /// + /// Throws the default indicating that a span length is outside a range. + /// + [ContractAnnotation("=> halt")] + [DoesNotReturn] + public static void SpanLengthNotInRange(ReadOnlySpan parameter, Range range, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) => throw new InvalidCollectionCountException(parameterName, message ?? $"{parameterName ?? "The span"} must have its length between {range.CreateRangeDescriptionText("and")}, but it actually has length {parameter.Length}."); + /// /// Throws the default indicating that a span is not longer than the /// specified length. /// diff --git a/ai-plans/0145-additional-assertions.md b/ai-plans/0145-additional-assertions.md new file mode 100644 index 00000000..d5f0af09 --- /dev/null +++ b/ai-plans/0145-additional-assertions.md @@ -0,0 +1,72 @@ +# Additional Assertions + +## Rationale + +Light.GuardClauses already covers the common null, equality, comparison, string, collection, date/time, type, enum, and URI preconditions, but several useful invariants and cross-container overloads are still missing. Add focused guards for UUIDv7 identifiers, finite floating-point values, ranged collection counts, canonical UTC `DateTimeOffset` values, and ASCII data, and complete the most useful `Span` and `Memory` length and content checks. + +The additions must preserve the library's existing fluent return values, exception-factory overloads, nullable annotations, broad target-framework support, Native AOT compatibility, and customizable single-file source distribution. UUIDv7 validation is performance-sensitive and must inspect the GUID structure without allocations on every supported target. + +## Acceptance Criteria + +- [x] `Guid` values can be tested with `IsUuidVersion7` and guarded with `MustBeUuidVersion7` on .NET Standard 2.0, .NET Standard 2.1, and .NET 10; a value is accepted only when both its UUID version and RFC/IETF variant bits identify UUIDv7. +- [x] Successful UUIDv7 checks perform no heap allocation and have a microbenchmark covering the Boolean check and successful throwing guard against an equivalent direct structural check. +- [x] The benchmark project targets and executes only .NET 10, with no .NET Framework target or legacy runtime job, so benchmarks can run consistently on every operating system supported by .NET 10 and BenchmarkDotNet. +- [x] `float` and `double` values can be tested and guarded for finiteness on all package targets, while the .NET 10 asset also supports generic IEEE 754 floating-point types such as `Half`. +- [x] General collections can require their count to fall within a `Range` through `MustHaveCountIn`, preserving the original collection shape and the established collection-count behavior. +- [x] Span and memory values have consistent non-empty and ranged-length guards, memory values have exact-length guards, and character span and memory values have a throwing counterpart to `IsEmptyOrWhiteSpace`. +- [x] `MustBeUtc` supports `DateTimeOffset` and accepts only values whose offset is `TimeSpan.Zero`, without changing the instant or returned value. +- [x] Characters, bytes, strings, and character/byte span and memory values can be tested and guarded as ASCII across all package targets; empty inputs are valid ASCII, while null strings are rejected by the throwing guard. +- [x] Every new throwing guard returns the successfully validated input, captures the guarded expression for the default exception, accepts an optional custom message, and provides an exception-factory overload consistent with the existing API for its input shape. +- [x] Automated tests cover successful values, every distinct failure condition, boundary values, default exceptions, custom messages, custom exception factories, caller argument expressions, return values, and target-specific overloads. +- [x] The source-export whitelist catalog and committed settings contain all new assertion families, framework flattening and reachability retain their required helpers, and focused source-export tests cover the new entries and relevant target-specific APIs. +- [x] The committed .NET Standard 2.0 single-file distribution is regenerated and validates with the complete new portable API surface. +- [x] The assertion overview and any affected usage or source-inclusion documentation describe the new families, their supported input shapes, target-specific overloads, and UUIDv7 structural-validation semantics. +- [x] The complete solution restores and builds without warnings in Release configuration, and all automated tests pass on the pinned SDK. + +## Technical Details + +Follow the existing convention of one `Check..cs` file per assertion family. Boolean checks are allocation-free predicates; throwing guards use `[CallerArgumentExpression]`, return the original value or collection shape, and offer the established default-exception and custom-factory overloads. Reuse the current exception hierarchy where it accurately describes the failure, preferring standard `ArgumentException` or `ArgumentOutOfRangeException` over adding narrowly useful public exception types. Span factories use the existing span delegate types so no ref struct is captured or boxed. + +**UUIDv7.** The exact public names are `IsUuidVersion7` and `MustBeUuidVersion7`. Validation is structural: the version field must be 7 and the variant must be the RFC/IETF `10xx` variant defined by RFC 9562. This inherently rejects `Guid.Empty`; do not add a redundant emptiness check or claim to verify timestamp provenance, entropy, randomness, or monotonic generation. + +Use one zero-allocation implementation across all target frameworks. Overlay the GUID with this exact internal type: + +```csharp +[StructLayout(LayoutKind.Explicit, Size = 16)] +internal readonly struct GuidLayout +{ + [FieldOffset(0)] + private readonly Guid _value; + + [FieldOffset(6)] + public readonly ushort TimeHighAndVersion; + + [FieldOffset(8)] + public readonly byte ClockSequenceHighAndReserved; + + public GuidLayout(Guid value) => _value = value; +} +``` + +The explicit overlay avoids array or string creation, unsafe compilation, and a dependency on `System.Runtime.CompilerServices.Unsafe`. Determine UUIDv7 with `(layout.TimeHighAndVersion & 0xF000) == 0x7000` and `(layout.ClockSequenceHighAndReserved & 0xC0) == 0x80`. Reading the version as the overlaid numeric field rather than indexing its component bytes keeps the check independent of machine endianness. Keep the layout helper reachable by the source exporter, document its dependency on the stable sequential GUID field layout, and cover it with tests using RFC UUIDv7 examples, `Guid.CreateVersion7()` on .NET 10, UUIDs of other versions, `Guid.Empty`, and a forged value with a version-7 nibble but a non-RFC variant. Add BenchmarkDotNet cases with memory diagnostics for valid and invalid Boolean checks and the successful guard path; the successful cases must report `0 B/op`. + +Make the benchmark project .NET 10-only by replacing its target-framework list with `net10.0` and removing the .NET Framework 4.8 runtime job. Replace the stale explicit .NET 8 job with one .NET 10 job. Enable the cross-platform memory diagnoser by default and make disassembly diagnostics opt-in through a benchmark configuration or command-line option so an unsupported disassembler cannot prevent ordinary benchmark execution. The default configuration must not depend on Windows, an installed .NET Framework runtime, or platform-specific diagnostic tooling. + +**Finite floating-point values.** Add `IsFinite` and `MustBeFinite` overloads for `float` and `double` to every target. The modern asset additionally exposes generic overloads constrained to `IFloatingPointIeee754` under the repository's existing modern-framework conditional, thereby covering `Half` without treating `decimal` as an IEEE 754 type. Finite values include positive and negative zero, subnormal values, and normal values; reject `NaN`, positive infinity, and negative infinity. The portable overloads must not depend on APIs absent from .NET Standard. + +**Collection count ranges.** Add `MustHaveCountIn(this TCollection? parameter, Range range, ...) where TCollection : class, IEnumerable`, plus its factory overload. Determine the count through the existing optimized collection-count helpers and enumerate a lazy enumerable no more than once per guard invocation. Null uses the existing null behavior; an out-of-range count uses `InvalidCollectionCountException` and reports the actual count and requested range. Preserve inclusive and exclusive endpoints exactly as modeled by `Range`. + +**Span and memory parity.** Extend the existing families rather than introducing alternate length terminology: + +- `MustNotBeEmpty` for `Span`, `ReadOnlySpan`, `Memory`, and `ReadOnlyMemory`; +- `MustHaveLengthIn` for those same four shapes; +- `MustHaveLength` for `Memory` and `ReadOnlyMemory`; and +- `MustNotBeEmptyOrWhiteSpace` for `Span`, `ReadOnlySpan`, `Memory`, and `ReadOnlyMemory`, implemented as the throwing counterpart to `IsEmptyOrWhiteSpace`. + +Mutable overloads should delegate validation to the read-only span implementation and return the original mutable value. Memory overloads must not allocate or copy their contents. Use `InvalidCollectionCountException` or the existing empty/string exception contracts consistently with the corresponding collection, length, and whitespace guards. + +**UTC `DateTimeOffset`.** Extend `MustBeUtc` with `DateTimeOffset` default and factory overloads. UTC means `parameter.Offset == TimeSpan.Zero`; do not convert a non-zero-offset value with `ToUniversalTime`, because a guard validates rather than normalizes. Continue using `InvalidDateTimeException`, updating its documentation and throw helpers to cover both `DateTime` and `DateTimeOffset` accurately. + +**ASCII.** Add `IsAscii` and `MustBeAscii` for `char`, `byte`, `string`, `Span`, `ReadOnlySpan`, `Memory`, `ReadOnlyMemory`, and the corresponding byte span and memory shapes. ASCII means every code unit or byte is at most `0x7F`; empty strings and buffers satisfy the predicate, `IsAscii(null)` is false, and `MustBeAscii` throws `ArgumentNullException` for a null string before checking content. Use the optimized framework implementation where appropriate on .NET 10 and a simple allocation-free portable loop elsewhere, with identical semantics across targets. Mutable and memory guards delegate to read-only span validation and return their original shape. + +Update `AssertionWhitelist`, `settings.json`, and source-export reachability tests for `IsUuidVersion7`, `MustBeUuidVersion7`, `IsFinite`, `MustBeFinite`, `MustHaveCountIn`, `MustNotBeEmptyOrWhiteSpace`, `IsAscii`, and `MustBeAscii`; additions that only extend existing files use their existing whitelist entries. Regenerate `Light.GuardClauses.SingleFile.cs` as the .NET Standard 2.0 output and verify both portable and .NET 10 generated-source validation. Update `docs/assertion-overview.md` as the primary user-facing catalog, and adjust other documentation where the overload matrix or source-export examples would otherwise become incomplete. diff --git a/benchmarks/Light.GuardClauses.Performance/CommonAssertions/UuidVersion7Benchmark.cs b/benchmarks/Light.GuardClauses.Performance/CommonAssertions/UuidVersion7Benchmark.cs new file mode 100644 index 00000000..63a638ca --- /dev/null +++ b/benchmarks/Light.GuardClauses.Performance/CommonAssertions/UuidVersion7Benchmark.cs @@ -0,0 +1,49 @@ +using System; +using System.Runtime.InteropServices; +using BenchmarkDotNet.Attributes; + +namespace Light.GuardClauses.Performance.CommonAssertions; + +[MemoryDiagnoser] +public class UuidVersion7Benchmark +{ + private readonly Guid _invalidUuid = Guid.NewGuid(); + private readonly Guid _validUuid = Guid.CreateVersion7(); + + [Benchmark(Baseline = true)] + public bool DirectValidStructuralCheck() => DirectCheck(_validUuid); + + [Benchmark] + public bool IsUuidVersion7Valid() => _validUuid.IsUuidVersion7(); + + [Benchmark] + public Guid MustBeUuidVersion7Valid() => _validUuid.MustBeUuidVersion7(); + + [Benchmark] + public bool DirectInvalidStructuralCheck() => DirectCheck(_invalidUuid); + + [Benchmark] + public bool IsUuidVersion7Invalid() => _invalidUuid.IsUuidVersion7(); + + private static bool DirectCheck(Guid value) + { + var layout = new BenchmarkGuidLayout(value); + return (layout.TimeHighAndVersion & 0xF000) == 0x7000 && + (layout.ClockSequenceHighAndReserved & 0xC0) == 0x80; + } + + [StructLayout(LayoutKind.Explicit, Size = 16)] + private readonly struct BenchmarkGuidLayout + { + [FieldOffset(0)] + private readonly Guid _value; + + [FieldOffset(6)] + public readonly ushort TimeHighAndVersion; + + [FieldOffset(8)] + public readonly byte ClockSequenceHighAndReserved; + + public BenchmarkGuidLayout(Guid value) => _value = value; + } +} diff --git a/benchmarks/Light.GuardClauses.Performance/Light.GuardClauses.Performance.csproj b/benchmarks/Light.GuardClauses.Performance/Light.GuardClauses.Performance.csproj index fca75f54..3177927c 100644 --- a/benchmarks/Light.GuardClauses.Performance/Light.GuardClauses.Performance.csproj +++ b/benchmarks/Light.GuardClauses.Performance/Light.GuardClauses.Performance.csproj @@ -2,7 +2,7 @@ Exe - net10.0;net48 + net10.0 diff --git a/benchmarks/Light.GuardClauses.Performance/Program.cs b/benchmarks/Light.GuardClauses.Performance/Program.cs index b17928a0..2c561d68 100644 --- a/benchmarks/Light.GuardClauses.Performance/Program.cs +++ b/benchmarks/Light.GuardClauses.Performance/Program.cs @@ -1,4 +1,5 @@ -using BenchmarkDotNet.Configs; +using System; +using BenchmarkDotNet.Configs; using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Environments; using BenchmarkDotNet.Jobs; @@ -8,13 +9,32 @@ namespace Light.GuardClauses.Performance; public static class Program { - private static IConfig DefaultConfiguration => - DefaultConfig - .Instance - .AddJob(Job.Default.WithRuntime(CoreRuntime.Core80)) - .AddJob(Job.Default.WithRuntime(ClrRuntime.Net48)) - .AddDiagnoser(MemoryDiagnoser.Default, new DisassemblyDiagnoser(new DisassemblyDiagnoserConfig())); + private const string EnableDisassemblyOption = "--enable-disassembly"; - public static void Main(string[] arguments) => - BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(arguments, DefaultConfiguration); -} \ No newline at end of file + private static IConfig CreateConfiguration(bool enableDisassembly) + { + var configuration = DefaultConfig + .Instance + .AddJob(Job.Default.WithRuntime(CoreRuntime.Core10_0)) + .AddDiagnoser(MemoryDiagnoser.Default); + + return enableDisassembly ? + configuration.AddDiagnoser(new DisassemblyDiagnoser(new DisassemblyDiagnoserConfig())) : + configuration; + } + + public static void Main(string[] arguments) + { + var enableDisassembly = Array.Exists( + arguments, + argument => string.Equals(argument, EnableDisassemblyOption, StringComparison.OrdinalIgnoreCase) + ); + var benchmarkArguments = Array.FindAll( + arguments, + argument => !string.Equals(argument, EnableDisassemblyOption, StringComparison.OrdinalIgnoreCase) + ); + + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly) + .Run(benchmarkArguments, CreateConfiguration(enableDisassembly)); + } +} diff --git a/docs/assertion-overview.md b/docs/assertion-overview.md index 99336376..901122c9 100644 --- a/docs/assertion-overview.md +++ b/docs/assertion-overview.md @@ -13,6 +13,7 @@ The package has .NET Standard 2.0, .NET Standard 2.1, and .NET 10 assets. The co The .NET 10 asset additionally provides: - generic `INumber` overloads for `IsApproximately`, `MustBeApproximately`, `MustNotBeApproximately`, `IsGreaterThanOrApproximately`, `MustBeGreaterThanOrApproximately`, `IsLessThanOrApproximately`, and `MustBeLessThanOrApproximately`; +- generic `IFloatingPointIeee754` overloads for `IsFinite` and `MustBeFinite`, including `Half` but excluding `decimal`; - `Span`, `ReadOnlySpan`, `Memory`, and `ReadOnlyMemory` overloads for `IsEmailAddress` and `MustBeEmailAddress`; and - trimming annotations on the type-relation helpers where supported by the framework. @@ -39,6 +40,9 @@ The seven `ImmutableArray` guard families are available across all package ta | `MustBeOfType` | Requires a value castable to `T` and returns the cast value | | `IsValidEnumValue`, `MustBeValidEnumValue` | Validate defined enum constants and valid flags combinations | | `IsEmpty`, `MustNotBeEmpty` | Test or reject `Guid.Empty` | +| `IsUuidVersion7`, `MustBeUuidVersion7` | Structurally test or require an RFC/IETF UUIDv7 | + +UUIDv7 validation checks the version-7 nibble and RFC/IETF `10xx` variant bits directly in the GUID layout without allocation. It does not claim to validate timestamp provenance, entropy, randomness, or monotonic generation. ## Condition and state assertions @@ -59,6 +63,7 @@ The seven `ImmutableArray` guard families are available across all package ta | `IsIn`, `MustBeIn` | Test or require membership in a `Range` | | `IsNotIn`, `MustNotBeIn` | Test or require non-membership in a `Range` | | `IsApproximately`, `MustBeApproximately`, `MustNotBeApproximately` | Compare floating-point values using a tolerance | +| `IsFinite`, `MustBeFinite` | Test or require finite `float` and `double` values; the .NET 10 asset also supports generic IEEE 754 types | | `IsGreaterThanOrApproximately`, `MustBeGreaterThanOrApproximately` | Accept values greater than or within tolerance of the comparison value | | `IsLessThanOrApproximately`, `MustBeLessThanOrApproximately` | Accept values less than or within tolerance of the comparison value | @@ -76,6 +81,7 @@ percentage.MustBeIn(Range.FromExclusive(0).ToInclusive(100)); | `IsNullOrEmpty` | Tests `IEnumerable` or `string` for null or emptiness | | `MustNotBeNullOrEmpty` | Requires a non-null, non-empty collection or string | | `MustHaveCount` | Requires an exact collection count | +| `MustHaveCountIn` | Requires a collection count within an inclusive/exclusive `Range` | | `MustHaveMinimumCount`, `MustHaveMaximumCount` | Enforce inclusive collection-count bounds | | `IsOneOf`, `MustBeOneOf`, `MustNotBeOneOf` | Test or enforce membership among supplied values | | `MustContain`, `MustNotContain` | Require or reject an item in collections and immutable arrays; string overloads operate on substrings | @@ -89,7 +95,8 @@ Collection overloads preserve and return the original collection shape where pos | Assertion family | Behavior | | --- | --- | | `IsNullOrWhiteSpace`, `MustNotBeNullOrWhiteSpace` | Test or reject null, empty, or all-whitespace strings | -| `IsEmptyOrWhiteSpace` | Tests character span/memory values for emptiness or whitespace | +| `IsEmptyOrWhiteSpace`, `MustNotBeEmptyOrWhiteSpace` | Test character span/memory values for, or reject, emptiness and all-whitespace content | +| `IsAscii`, `MustBeAscii` | Test or require ASCII characters, bytes, strings, and character/byte span or memory values; empty inputs are valid | | `IsWhiteSpace`, `IsLetter`, `IsLetterOrDigit`, `IsDigit` | Character classification | | `IsNewLine`, `MustBeNewLine` | Recognize or require `"\n"` or `"\r\n"`, independently of the platform newline | | `IsTrimmed`, `MustBeTrimmed` | Test or require no leading or trailing whitespace | @@ -102,7 +109,8 @@ Collection overloads preserve and return the original collection shape where pos | `IsSubstringOf`, `MustBeSubstringOf`, `MustNotBeSubstringOf` | Test, require, or reject substring relationships | | `MustContain`, `MustNotContain` | Require or reject substrings, with comparison overloads | | `MustStartWith`, `MustNotStartWith`, `MustEndWith`, `MustNotEndWith` | Enforce string boundaries; `MustNotStartWith` also has span overloads | -| `MustHaveLength`, `MustHaveLengthIn` | Enforce exact or ranged string lengths; `MustHaveLength` also supports spans and immutable arrays | +| `MustHaveLength`, `MustHaveLengthIn` | Enforce exact or ranged lengths for strings, spans, memory, and immutable arrays as provided by their overloads | +| `MustNotBeEmpty` | Reject empty spans and memory while preserving the mutable or read-only input shape | | `MustBeLongerThan`, `MustBeLongerThanOrEqualTo` | Enforce lower string/span length bounds | | `MustBeShorterThan`, `MustBeShorterThanOrEqualTo` | Enforce upper string/span length bounds | @@ -112,7 +120,7 @@ The `Light.GuardClauses.FrameworkExtensions.StringExtensions.Contains` companion | Assertion | Behavior | | --- | --- | -| `MustBeUtc` | Requires `DateTimeKind.Utc` | +| `MustBeUtc` | Requires `DateTimeKind.Utc` for `DateTime`, or `TimeSpan.Zero` offset for `DateTimeOffset`; values are validated without conversion | | `MustBeLocal` | Requires `DateTimeKind.Local` | | `MustBeUnspecified` | Requires `DateTimeKind.Unspecified` | diff --git a/docs/contributing-and-building.md b/docs/contributing-and-building.md index 8744f54c..ef718c74 100644 --- a/docs/contributing-and-building.md +++ b/docs/contributing-and-building.md @@ -16,7 +16,7 @@ dotnet build Light.GuardClauses.slnx -c Release --no-restore dotnet test Light.GuardClauses.slnx -c Release --no-build ``` -The product package targets .NET Standard 2.0, .NET Standard 2.1, and .NET 10. Tests and tools run on .NET 10 by default. The benchmark project targets .NET 10 and .NET Framework 4.8; running its .NET Framework target requires a compatible Windows environment. +The product package targets .NET Standard 2.0, .NET Standard 2.1, and .NET 10. Tests, tools, and benchmarks run on .NET 10. BenchmarkDotNet memory diagnostics are enabled by default; pass `--enable-disassembly` after the benchmark project's `--` separator to opt into platform-dependent disassembly diagnostics. ## Repository layout diff --git a/docs/historical-performance.md b/docs/historical-performance.md index af642672..eb27cffd 100644 --- a/docs/historical-performance.md +++ b/docs/historical-performance.md @@ -4,7 +4,7 @@ > **Archive notice:** These results were measured in early August 2018 against Light.GuardClauses 5.0 and 3.5, BenchmarkDotNet 0.11.0, .NET Core 2.1.2, and .NET Framework 4.7.2. They are preserved as project history. They do not describe the current release, .NET 10, current JITs, or current hardware. -For reproducible measurements of the current code, run the [current BenchmarkDotNet project](../benchmarks/Light.GuardClauses.Performance/). It targets .NET 10 and .NET Framework 4.8; benchmark source and configuration, rather than this archive, are the authority for current scenarios. +For reproducible measurements of the current code, run the [current BenchmarkDotNet project](../benchmarks/Light.GuardClauses.Performance/). It targets .NET 10; benchmark source and configuration, rather than this archive, are the authority for current scenarios. The archived tables compare: diff --git a/docs/source-code-inclusion.md b/docs/source-code-inclusion.md index 484ae8c3..65630a6e 100644 --- a/docs/source-code-inclusion.md +++ b/docs/source-code-inclusion.md @@ -61,7 +61,7 @@ Use the committed settings file for the exact names and defaults of all nullable `TargetFramework` selects exactly one source shape: - `NetStandard2_0` is the portable shape used by the committed single-file distribution. -- `Net10_0` includes modern generic-math overloads and the .NET 10 span/memory email APIs. It suppresses code-analysis and caller-argument-expression polyfills already supplied by the framework. +- `Net10_0` includes modern generic-math and generic IEEE 754 finiteness overloads, optimized ASCII checks, and the .NET 10 span/memory email APIs. It suppresses code-analysis and caller-argument-expression polyfills already supplied by the framework. The parser selects the appropriate conditional branches and the merger emits no `#if`, `#else`, `#endif`, or other conditional-compilation directives. The result is therefore for the selected target only, not a multi-target source file. diff --git a/src/Light.GuardClauses/Check.IsAscii.cs b/src/Light.GuardClauses/Check.IsAscii.cs new file mode 100644 index 00000000..837a7f57 --- /dev/null +++ b/src/Light.GuardClauses/Check.IsAscii.cs @@ -0,0 +1,84 @@ +using System; +using System.Runtime.CompilerServices; +#if NET8_0_OR_GREATER +using System.Text; +#endif + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// Checks if the character is an ASCII code point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAscii(this char parameter) => parameter <= 0x7F; + + /// Checks if the byte is an ASCII value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAscii(this byte parameter) => parameter <= 0x7F; + + /// Checks if the string is non-null and contains only ASCII characters. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAscii(this string? parameter) => parameter is not null && parameter.AsSpan().IsAscii(); + + /// Checks if the character span contains only ASCII characters. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAscii(this Span parameter) => ((ReadOnlySpan) parameter).IsAscii(); + + /// Checks if the read-only character span contains only ASCII characters. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAscii(this ReadOnlySpan parameter) + { +#if NET8_0_OR_GREATER + return Ascii.IsValid(parameter); +#else + foreach (var value in parameter) + { + if (value > 0x7F) + { + return false; + } + } + + return true; +#endif + } + + /// Checks if the character memory contains only ASCII characters. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAscii(this Memory parameter) => parameter.Span.IsAscii(); + + /// Checks if the read-only character memory contains only ASCII characters. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAscii(this ReadOnlyMemory parameter) => parameter.Span.IsAscii(); + + /// Checks if the byte span contains only ASCII values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAscii(this Span parameter) => ((ReadOnlySpan) parameter).IsAscii(); + + /// Checks if the read-only byte span contains only ASCII values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAscii(this ReadOnlySpan parameter) + { +#if NET8_0_OR_GREATER + return Ascii.IsValid(parameter); +#else + foreach (var value in parameter) + { + if (value > 0x7F) + { + return false; + } + } + + return true; +#endif + } + + /// Checks if the byte memory contains only ASCII values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAscii(this Memory parameter) => parameter.Span.IsAscii(); + + /// Checks if the read-only byte memory contains only ASCII values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAscii(this ReadOnlyMemory parameter) => parameter.Span.IsAscii(); +} diff --git a/src/Light.GuardClauses/Check.IsFinite.cs b/src/Light.GuardClauses/Check.IsFinite.cs new file mode 100644 index 00000000..2cdf0e0c --- /dev/null +++ b/src/Light.GuardClauses/Check.IsFinite.cs @@ -0,0 +1,30 @@ +using System.Runtime.CompilerServices; +#if NET8_0_OR_GREATER +using System.Numerics; +#endif + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// + /// Checks if the specified single-precision floating-point value is finite. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsFinite(this float parameter) => parameter >= float.MinValue && parameter <= float.MaxValue; + + /// + /// Checks if the specified double-precision floating-point value is finite. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsFinite(this double parameter) => parameter >= double.MinValue && parameter <= double.MaxValue; + +#if NET8_0_OR_GREATER + /// + /// Checks if the specified IEEE 754 floating-point value is finite. + /// + /// The IEEE 754 floating-point type. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsFinite(this T parameter) where T : IFloatingPointIeee754 => T.IsFinite(parameter); +#endif +} diff --git a/src/Light.GuardClauses/Check.IsUuidVersion7.cs b/src/Light.GuardClauses/Check.IsUuidVersion7.cs new file mode 100644 index 00000000..7c527096 --- /dev/null +++ b/src/Light.GuardClauses/Check.IsUuidVersion7.cs @@ -0,0 +1,20 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// + /// Checks if the specified GUID structurally identifies an RFC/IETF UUID version 7. + /// + /// The GUID to be checked. + /// True when the UUID version is 7 and the variant is RFC/IETF, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsUuidVersion7(this Guid parameter) + { + var layout = new GuidLayout(parameter); + return (layout.TimeHighAndVersion & 0xF000) == 0x7000 && + (layout.ClockSequenceHighAndReserved & 0xC0) == 0x80; + } +} diff --git a/src/Light.GuardClauses/Check.MustBeAscii.cs b/src/Light.GuardClauses/Check.MustBeAscii.cs new file mode 100644 index 00000000..1951ec9b --- /dev/null +++ b/src/Light.GuardClauses/Check.MustBeAscii.cs @@ -0,0 +1,302 @@ +using System; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using Light.GuardClauses.ExceptionFactory; +using NotNullAttribute = System.Diagnostics.CodeAnalysis.NotNullAttribute; + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// Ensures that the character is ASCII, or otherwise throws an . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static char MustBeAscii( + this char parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (!parameter.IsAscii()) + { + Throw.Argument(parameterName, message ?? $"{parameterName ?? "The character"} must be ASCII, but it actually is '{parameter}'."); + } + + return parameter; + } + + /// Ensures that the character is ASCII, or otherwise throws your custom exception. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static char MustBeAscii(this char parameter, Func exceptionFactory) + { + if (!parameter.IsAscii()) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// Ensures that the byte is ASCII, or otherwise throws an . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte MustBeAscii( + this byte parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (!parameter.IsAscii()) + { + Throw.Argument(parameterName, message ?? $"{parameterName ?? "The byte"} must be ASCII, but it actually is {parameter}."); + } + + return parameter; + } + + /// Ensures that the byte is ASCII, or otherwise throws your custom exception. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static byte MustBeAscii(this byte parameter, Func exceptionFactory) + { + if (!parameter.IsAscii()) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// Ensures that the string is non-null and contains only ASCII characters. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static string MustBeAscii( + [NotNull] [ValidatedNotNull] this string? parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + parameter.MustNotBeNull(parameterName, message); + if (!parameter.IsAscii()) + { + Throw.Argument(parameterName, message ?? $"{parameterName ?? "The string"} must contain only ASCII characters."); + } + + return parameter; + } + + /// Ensures that the string is non-null and contains only ASCII characters, or otherwise throws your custom exception. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull; exceptionFactory:null => halt")] + public static string MustBeAscii( + [NotNull] [ValidatedNotNull] this string? parameter, + Func exceptionFactory + ) + { + if (parameter is null || !parameter.IsAscii()) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// Ensures that the character span contains only ASCII characters. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeAscii( + this Span parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter).MustBeAscii(parameterName, message); + return parameter; + } + + /// Ensures that the character span contains only ASCII characters, or otherwise throws your custom exception. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeAscii( + this Span parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + ((ReadOnlySpan) parameter).MustBeAscii(exceptionFactory); + return parameter; + } + + /// Ensures that the read-only character span contains only ASCII characters. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan MustBeAscii( + this ReadOnlySpan parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (!parameter.IsAscii()) + { + Throw.Argument(parameterName, message ?? $"{parameterName ?? "The character span"} must contain only ASCII characters."); + } + + return parameter; + } + + /// Ensures that the read-only character span contains only ASCII characters, or otherwise throws your custom exception. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan MustBeAscii( + this ReadOnlySpan parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + if (!parameter.IsAscii()) + { + Throw.CustomSpanException(exceptionFactory, parameter); + } + + return parameter; + } + + /// Ensures that the character memory contains only ASCII characters. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeAscii( + this Memory parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter.Span).MustBeAscii(parameterName, message); + return parameter; + } + + /// Ensures that the character memory contains only ASCII characters, or otherwise throws your custom exception. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeAscii( + this Memory parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + ((ReadOnlySpan) parameter.Span).MustBeAscii(exceptionFactory); + return parameter; + } + + /// Ensures that the read-only character memory contains only ASCII characters. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeAscii( + this ReadOnlyMemory parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + parameter.Span.MustBeAscii(parameterName, message); + return parameter; + } + + /// Ensures that the read-only character memory contains only ASCII characters, or otherwise throws your custom exception. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeAscii( + this ReadOnlyMemory parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + parameter.Span.MustBeAscii(exceptionFactory); + return parameter; + } + + /// Ensures that the byte span contains only ASCII values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeAscii( + this Span parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter).MustBeAscii(parameterName, message); + return parameter; + } + + /// Ensures that the byte span contains only ASCII values, or otherwise throws your custom exception. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeAscii( + this Span parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + ((ReadOnlySpan) parameter).MustBeAscii(exceptionFactory); + return parameter; + } + + /// Ensures that the read-only byte span contains only ASCII values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan MustBeAscii( + this ReadOnlySpan parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (!parameter.IsAscii()) + { + Throw.Argument(parameterName, message ?? $"{parameterName ?? "The byte span"} must contain only ASCII values."); + } + + return parameter; + } + + /// Ensures that the read-only byte span contains only ASCII values, or otherwise throws your custom exception. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan MustBeAscii( + this ReadOnlySpan parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + if (!parameter.IsAscii()) + { + Throw.CustomSpanException(exceptionFactory, parameter); + } + + return parameter; + } + + /// Ensures that the byte memory contains only ASCII values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeAscii( + this Memory parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter.Span).MustBeAscii(parameterName, message); + return parameter; + } + + /// Ensures that the byte memory contains only ASCII values, or otherwise throws your custom exception. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeAscii( + this Memory parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + ((ReadOnlySpan) parameter.Span).MustBeAscii(exceptionFactory); + return parameter; + } + + /// Ensures that the read-only byte memory contains only ASCII values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeAscii( + this ReadOnlyMemory parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + parameter.Span.MustBeAscii(parameterName, message); + return parameter; + } + + /// Ensures that the read-only byte memory contains only ASCII values, or otherwise throws your custom exception. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeAscii( + this ReadOnlyMemory parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + parameter.Span.MustBeAscii(exceptionFactory); + return parameter; + } +} diff --git a/src/Light.GuardClauses/Check.MustBeFinite.cs b/src/Light.GuardClauses/Check.MustBeFinite.cs new file mode 100644 index 00000000..11261945 --- /dev/null +++ b/src/Light.GuardClauses/Check.MustBeFinite.cs @@ -0,0 +1,116 @@ +using System; +#if NET8_0_OR_GREATER +using System.Numerics; +#endif +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using Light.GuardClauses.ExceptionFactory; + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// + /// Ensures that the specified single-precision floating-point value is finite, or otherwise throws an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MustBeFinite( + this float parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (!parameter.IsFinite()) + { + Throw.NotFinite(parameter, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified single-precision floating-point value is finite, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static float MustBeFinite(this float parameter, Func exceptionFactory) + { + if (!parameter.IsFinite()) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the specified double-precision floating-point value is finite, or otherwise throws an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double MustBeFinite( + this double parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (!parameter.IsFinite()) + { + Throw.NotFinite(parameter, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified double-precision floating-point value is finite, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static double MustBeFinite(this double parameter, Func exceptionFactory) + { + if (!parameter.IsFinite()) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + +#if NET8_0_OR_GREATER + /// + /// Ensures that the specified IEEE 754 floating-point value is finite, or otherwise throws an . + /// + /// The IEEE 754 floating-point type. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T MustBeFinite( + this T parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) where T : IFloatingPointIeee754 + { + if (!T.IsFinite(parameter)) + { + Throw.NotFinite(parameter, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified IEEE 754 floating-point value is finite, or otherwise throws your custom exception. + /// + /// The IEEE 754 floating-point type. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static T MustBeFinite(this T parameter, Func exceptionFactory) + where T : IFloatingPointIeee754 + { + if (!T.IsFinite(parameter)) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } +#endif +} diff --git a/src/Light.GuardClauses/Check.MustBeUtc.cs b/src/Light.GuardClauses/Check.MustBeUtc.cs index 87cbc2bd..5fa35925 100644 --- a/src/Light.GuardClauses/Check.MustBeUtc.cs +++ b/src/Light.GuardClauses/Check.MustBeUtc.cs @@ -47,4 +47,49 @@ public static DateTime MustBeUtc(this DateTime parameter, Func + /// Ensures that the specified has a zero offset, or otherwise throws an . + /// + /// The date time offset to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The original date time offset without conversion. + /// Thrown when does not have as its offset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static DateTimeOffset MustBeUtc( + this DateTimeOffset parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (parameter.Offset != TimeSpan.Zero) + { + Throw.MustBeUtcDateTimeOffset(parameter, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified has a zero offset, or otherwise throws your custom exception. + /// + /// The date time offset to be checked. + /// The delegate that creates your custom exception. is passed to this delegate. + /// The original date time offset without conversion. + /// Your custom exception thrown when does not have as its offset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static DateTimeOffset MustBeUtc( + this DateTimeOffset parameter, + Func exceptionFactory + ) + { + if (parameter.Offset != TimeSpan.Zero) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } } diff --git a/src/Light.GuardClauses/Check.MustBeUuidVersion7.cs b/src/Light.GuardClauses/Check.MustBeUuidVersion7.cs new file mode 100644 index 00000000..80d16a58 --- /dev/null +++ b/src/Light.GuardClauses/Check.MustBeUuidVersion7.cs @@ -0,0 +1,54 @@ +using System; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using Light.GuardClauses.ExceptionFactory; + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// + /// Ensures that the specified GUID structurally identifies an RFC/IETF UUID version 7, or otherwise throws an . + /// + /// The GUID to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The original GUID. + /// Thrown when the UUID version is not 7 or the variant is not RFC/IETF. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Guid MustBeUuidVersion7( + this Guid parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (!parameter.IsUuidVersion7()) + { + Throw.Argument( + parameterName, + message ?? $"{parameterName ?? "The GUID"} must be an RFC/IETF UUID version 7, but it actually is \"{parameter}\"." + ); + } + + return parameter; + } + + /// + /// Ensures that the specified GUID structurally identifies an RFC/IETF UUID version 7, or otherwise throws your custom exception. + /// + /// The GUID to be checked. + /// The delegate that creates your custom exception. is passed to this delegate. + /// The original GUID. + /// Your custom exception thrown when the UUID version is not 7 or the variant is not RFC/IETF. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static Guid MustBeUuidVersion7(this Guid parameter, Func exceptionFactory) + { + if (!parameter.IsUuidVersion7()) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } +} diff --git a/src/Light.GuardClauses/Check.MustHaveCountIn.cs b/src/Light.GuardClauses/Check.MustHaveCountIn.cs new file mode 100644 index 00000000..14218ef4 --- /dev/null +++ b/src/Light.GuardClauses/Check.MustHaveCountIn.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using Light.GuardClauses.ExceptionFactory; +using Light.GuardClauses.Exceptions; +using Light.GuardClauses.FrameworkExtensions; +using NotNullAttribute = System.Diagnostics.CodeAnalysis.NotNullAttribute; + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// + /// Ensures that the collection count is within the specified range, or otherwise throws an . + /// + /// The collection to be checked. + /// The range in which the collection count must lie. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The original collection. + /// Thrown when the collection count is not within . + /// Thrown when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static TCollection MustHaveCountIn( + [NotNull] [ValidatedNotNull] this TCollection? parameter, + Range range, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) where TCollection : class, IEnumerable + { + var actualCount = parameter.Count(parameterName, message); + if (!range.IsValueWithinRange(actualCount)) + { + Throw.CollectionCountNotInRange(parameter, actualCount, range, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the collection count is within the specified range, or otherwise throws your custom exception. + /// + /// The collection to be checked. + /// The range in which the collection count must lie. + /// The delegate that creates your custom exception. and are passed to this delegate. + /// The original collection. + /// Your custom exception thrown when the collection is null or its count is not within . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull; exceptionFactory:null => halt")] + public static TCollection MustHaveCountIn( + [NotNull] [ValidatedNotNull] this TCollection? parameter, + Range range, + Func, Exception> exceptionFactory + ) where TCollection : class, IEnumerable + { + if (parameter is null || !range.IsValueWithinRange(parameter.Count())) + { + Throw.CustomException(exceptionFactory, parameter, range); + } + + return parameter; + } +} diff --git a/src/Light.GuardClauses/Check.MustHaveLength.cs b/src/Light.GuardClauses/Check.MustHaveLength.cs index 9277d9a3..059a689e 100644 --- a/src/Light.GuardClauses/Check.MustHaveLength.cs +++ b/src/Light.GuardClauses/Check.MustHaveLength.cs @@ -125,6 +125,64 @@ public static ReadOnlySpan MustHaveLength( return parameter; } + /// + /// Ensures that the memory has the specified length, or otherwise throws an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustHaveLength( + this Memory parameter, + int length, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter.Span).MustHaveLength(length, parameterName, message); + return parameter; + } + + /// + /// Ensures that the memory has the specified length, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustHaveLength( + this Memory parameter, + int length, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + parameter.Span.MustHaveLength(length, exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only memory has the specified length, or otherwise throws an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustHaveLength( + this ReadOnlyMemory parameter, + int length, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + parameter.Span.MustHaveLength(length, parameterName, message); + return parameter; + } + + /// + /// Ensures that the read-only memory has the specified length, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustHaveLength( + this ReadOnlyMemory parameter, + int length, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + parameter.Span.MustHaveLength(length, exceptionFactory); + return parameter; + } + /// /// Ensures that the span has the specified length, or otherwise throws your custom exception. /// diff --git a/src/Light.GuardClauses/Check.MustHaveLengthIn.cs b/src/Light.GuardClauses/Check.MustHaveLengthIn.cs index f0cb16ea..90e26787 100644 --- a/src/Light.GuardClauses/Check.MustHaveLengthIn.cs +++ b/src/Light.GuardClauses/Check.MustHaveLengthIn.cs @@ -107,4 +107,128 @@ Func, Range, Exception> exceptionFactory return parameter; } + + /// + /// Ensures that the span length is within the specified range, or otherwise throws an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustHaveLengthIn( + this Span parameter, + Range range, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter).MustHaveLengthIn(range, parameterName, message); + return parameter; + } + + /// + /// Ensures that the span length is within the specified range, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustHaveLengthIn( + this Span parameter, + Range range, + ReadOnlySpanExceptionFactory> exceptionFactory + ) + { + ((ReadOnlySpan) parameter).MustHaveLengthIn(range, exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only span length is within the specified range, or otherwise throws an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan MustHaveLengthIn( + this ReadOnlySpan parameter, + Range range, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (!range.IsValueWithinRange(parameter.Length)) + { + Throw.SpanLengthNotInRange(parameter, range, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the read-only span length is within the specified range, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan MustHaveLengthIn( + this ReadOnlySpan parameter, + Range range, + ReadOnlySpanExceptionFactory> exceptionFactory + ) + { + if (!range.IsValueWithinRange(parameter.Length)) + { + Throw.CustomSpanException(exceptionFactory, parameter, range); + } + + return parameter; + } + + /// + /// Ensures that the memory length is within the specified range, or otherwise throws an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustHaveLengthIn( + this Memory parameter, + Range range, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter.Span).MustHaveLengthIn(range, parameterName, message); + return parameter; + } + + /// + /// Ensures that the memory length is within the specified range, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustHaveLengthIn( + this Memory parameter, + Range range, + ReadOnlySpanExceptionFactory> exceptionFactory + ) + { + ((ReadOnlySpan) parameter.Span).MustHaveLengthIn(range, exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only memory length is within the specified range, or otherwise throws an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustHaveLengthIn( + this ReadOnlyMemory parameter, + Range range, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + parameter.Span.MustHaveLengthIn(range, parameterName, message); + return parameter; + } + + /// + /// Ensures that the read-only memory length is within the specified range, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustHaveLengthIn( + this ReadOnlyMemory parameter, + Range range, + ReadOnlySpanExceptionFactory> exceptionFactory + ) + { + parameter.Span.MustHaveLengthIn(range, exceptionFactory); + return parameter; + } } diff --git a/src/Light.GuardClauses/Check.MustNotBeEmpty.cs b/src/Light.GuardClauses/Check.MustNotBeEmpty.cs index 675e769b..7075d0e4 100644 --- a/src/Light.GuardClauses/Check.MustNotBeEmpty.cs +++ b/src/Light.GuardClauses/Check.MustNotBeEmpty.cs @@ -45,4 +45,120 @@ public static Guid MustNotBeEmpty(this Guid parameter, Func exception } return parameter; } + + /// + /// Ensures that the specified span is not empty, or otherwise throws an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustNotBeEmpty( + this Span parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter).MustNotBeEmpty(parameterName, message); + return parameter; + } + + /// + /// Ensures that the specified span is not empty, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustNotBeEmpty( + this Span parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + ((ReadOnlySpan) parameter).MustNotBeEmpty(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the specified read-only span is not empty, or otherwise throws an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan MustNotBeEmpty( + this ReadOnlySpan parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (parameter.IsEmpty) + { + Throw.EmptyCollection(parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified read-only span is not empty, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan MustNotBeEmpty( + this ReadOnlySpan parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + if (parameter.IsEmpty) + { + Throw.CustomSpanException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the specified memory is not empty, or otherwise throws an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustNotBeEmpty( + this Memory parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter.Span).MustNotBeEmpty(parameterName, message); + return parameter; + } + + /// + /// Ensures that the specified memory is not empty, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustNotBeEmpty( + this Memory parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + ((ReadOnlySpan) parameter.Span).MustNotBeEmpty(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the specified read-only memory is not empty, or otherwise throws an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustNotBeEmpty( + this ReadOnlyMemory parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + parameter.Span.MustNotBeEmpty(parameterName, message); + return parameter; + } + + /// + /// Ensures that the specified read-only memory is not empty, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustNotBeEmpty( + this ReadOnlyMemory parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + parameter.Span.MustNotBeEmpty(exceptionFactory); + return parameter; + } } diff --git a/src/Light.GuardClauses/Check.MustNotBeEmptyOrWhiteSpace.cs b/src/Light.GuardClauses/Check.MustNotBeEmptyOrWhiteSpace.cs new file mode 100644 index 00000000..e9208a6f --- /dev/null +++ b/src/Light.GuardClauses/Check.MustNotBeEmptyOrWhiteSpace.cs @@ -0,0 +1,132 @@ +using System; +using System.Runtime.CompilerServices; +using Light.GuardClauses.ExceptionFactory; +using Light.GuardClauses.Exceptions; + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// + /// Ensures that the character span is neither empty nor all white space. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustNotBeEmptyOrWhiteSpace( + this Span parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter).MustNotBeEmptyOrWhiteSpace(parameterName, message); + return parameter; + } + + /// + /// Ensures that the character span is neither empty nor all white space, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustNotBeEmptyOrWhiteSpace( + this Span parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + ((ReadOnlySpan) parameter).MustNotBeEmptyOrWhiteSpace(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only character span is neither empty nor all white space. + /// + /// Thrown when is empty. + /// Thrown when contains only white space. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan MustNotBeEmptyOrWhiteSpace( + this ReadOnlySpan parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (parameter.IsEmpty) + { + Throw.EmptyString(parameterName, message); + } + + if (parameter.IsEmptyOrWhiteSpace()) + { + Throw.WhiteSpaceSpan(parameter, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the read-only character span is neither empty nor all white space, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan MustNotBeEmptyOrWhiteSpace( + this ReadOnlySpan parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + if (parameter.IsEmptyOrWhiteSpace()) + { + Throw.CustomSpanException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the character memory is neither empty nor all white space. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustNotBeEmptyOrWhiteSpace( + this Memory parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter.Span).MustNotBeEmptyOrWhiteSpace(parameterName, message); + return parameter; + } + + /// + /// Ensures that the character memory is neither empty nor all white space, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustNotBeEmptyOrWhiteSpace( + this Memory parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + ((ReadOnlySpan) parameter.Span).MustNotBeEmptyOrWhiteSpace(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only character memory is neither empty nor all white space. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustNotBeEmptyOrWhiteSpace( + this ReadOnlyMemory parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + parameter.Span.MustNotBeEmptyOrWhiteSpace(parameterName, message); + return parameter; + } + + /// + /// Ensures that the read-only character memory is neither empty nor all white space, or otherwise throws your custom exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustNotBeEmptyOrWhiteSpace( + this ReadOnlyMemory parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + parameter.Span.MustNotBeEmptyOrWhiteSpace(exceptionFactory); + return parameter; + } +} diff --git a/src/Light.GuardClauses/ExceptionFactory/Throw.CollectionCountNotInRange.cs b/src/Light.GuardClauses/ExceptionFactory/Throw.CollectionCountNotInRange.cs new file mode 100644 index 00000000..fdc18668 --- /dev/null +++ b/src/Light.GuardClauses/ExceptionFactory/Throw.CollectionCountNotInRange.cs @@ -0,0 +1,28 @@ +using System.Collections; +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 a collection count is outside a range. + /// + [ContractAnnotation("=> halt")] + [DoesNotReturn] + public static void CollectionCountNotInRange( + IEnumerable parameter, + int actualCount, + Range range, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) => + throw new InvalidCollectionCountException( + parameterName, + message ?? + $"{parameterName ?? "The collection"} must have its count between {range.CreateRangeDescriptionText("and")}, but it actually has count {actualCount}." + ); +} diff --git a/src/Light.GuardClauses/ExceptionFactory/Throw.DateTime.cs b/src/Light.GuardClauses/ExceptionFactory/Throw.DateTime.cs index 865b60c2..4a7e874c 100644 --- a/src/Light.GuardClauses/ExceptionFactory/Throw.DateTime.cs +++ b/src/Light.GuardClauses/ExceptionFactory/Throw.DateTime.cs @@ -25,6 +25,23 @@ public static void MustBeUtcDateTime( $"{parameterName ?? "The date time"} must use kind \"{DateTimeKind.Utc}\", but it actually uses \"{parameter.Kind}\" and is \"{parameter:O}\"." ); + /// + /// Throws the default indicating that a date time offset does not use + /// as its offset, using the optional parameter name and message. + /// + [ContractAnnotation("=> halt")] + [DoesNotReturn] + public static void MustBeUtcDateTimeOffset( + DateTimeOffset parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) => + throw new InvalidDateTimeException( + parameterName, + message ?? + $"{parameterName ?? "The date time offset"} must use offset \"{TimeSpan.Zero}\", but it actually uses \"{parameter.Offset}\" and is \"{parameter:O}\"." + ); + /// /// Throws the default indicating that a date time is not using /// , using the optional parameter name and message. diff --git a/src/Light.GuardClauses/ExceptionFactory/Throw.NotFinite.cs b/src/Light.GuardClauses/ExceptionFactory/Throw.NotFinite.cs new file mode 100644 index 00000000..46bd984c --- /dev/null +++ b/src/Light.GuardClauses/ExceptionFactory/Throw.NotFinite.cs @@ -0,0 +1,24 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; + +namespace Light.GuardClauses.ExceptionFactory; + +public static partial class Throw +{ + /// + /// Throws an indicating that a floating-point value is not finite. + /// + [ContractAnnotation("=> halt")] + [DoesNotReturn] + public static void NotFinite( + T parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) => + throw new ArgumentOutOfRangeException( + parameterName, + message ?? $"{parameterName ?? "The value"} must be finite, but it actually is {parameter}." + ); +} diff --git a/src/Light.GuardClauses/ExceptionFactory/Throw.Span.cs b/src/Light.GuardClauses/ExceptionFactory/Throw.Span.cs index a3c04bf2..96292be4 100644 --- a/src/Light.GuardClauses/ExceptionFactory/Throw.Span.cs +++ b/src/Light.GuardClauses/ExceptionFactory/Throw.Span.cs @@ -26,6 +26,23 @@ public static void InvalidSpanLength( $"{parameterName ?? "The read-only span"} must have length {length}, but it actually has length {parameter.Length}." ); + /// + /// Throws the default indicating that a span length is outside a range. + /// + [ContractAnnotation("=> halt")] + [DoesNotReturn] + public static void SpanLengthNotInRange( + ReadOnlySpan parameter, + Range range, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) => + throw new InvalidCollectionCountException( + parameterName, + message ?? + $"{parameterName ?? "The span"} must have its length between {range.CreateRangeDescriptionText("and")}, but it actually has length {parameter.Length}." + ); + /// /// Throws the default indicating that a span is not longer than the /// specified length. diff --git a/src/Light.GuardClauses/ExceptionFactory/Throw.WhiteSpaceString.cs b/src/Light.GuardClauses/ExceptionFactory/Throw.WhiteSpaceString.cs index bae38b27..29ad26c6 100644 --- a/src/Light.GuardClauses/ExceptionFactory/Throw.WhiteSpaceString.cs +++ b/src/Light.GuardClauses/ExceptionFactory/Throw.WhiteSpaceString.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using JetBrains.Annotations; @@ -23,4 +24,21 @@ public static void WhiteSpaceString( message ?? $"{parameterName ?? "The string"} must not contain only white space, but it actually is \"{parameter}\"." ); + + /// + /// Throws the default indicating that a character span contains only + /// white space, using the optional parameter name and message. + /// + [ContractAnnotation("=> halt")] + [DoesNotReturn] + public static void WhiteSpaceSpan( + ReadOnlySpan parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) => + throw new WhiteSpaceStringException( + parameterName, + message ?? + $"{parameterName ?? "The character span"} must not contain only white space, but it actually has length {parameter.Length}." + ); } diff --git a/src/Light.GuardClauses/Exceptions/InvalidDateTimeException.cs b/src/Light.GuardClauses/Exceptions/InvalidDateTimeException.cs index 28605dc3..49ec5170 100644 --- a/src/Light.GuardClauses/Exceptions/InvalidDateTimeException.cs +++ b/src/Light.GuardClauses/Exceptions/InvalidDateTimeException.cs @@ -4,7 +4,7 @@ namespace Light.GuardClauses.Exceptions; /// -/// This exception indicates that a value is invalid. +/// This exception indicates that a or value is invalid. /// [Serializable] public class InvalidDateTimeException : ArgumentException @@ -20,4 +20,4 @@ public InvalidDateTimeException(string? parameterName = null, string? message = /// protected InvalidDateTimeException(SerializationInfo info, StreamingContext context) : base(info, context) { } #endif -} \ No newline at end of file +} diff --git a/src/Light.GuardClauses/GuidLayout.cs b/src/Light.GuardClauses/GuidLayout.cs new file mode 100644 index 00000000..d0c70ed4 --- /dev/null +++ b/src/Light.GuardClauses/GuidLayout.cs @@ -0,0 +1,22 @@ +using System; +using System.Runtime.InteropServices; + +namespace Light.GuardClauses; + +/// +/// Overlays the stable sequential GUID field layout so UUID structural fields can be inspected without allocations. +/// +[StructLayout(LayoutKind.Explicit, Size = 16)] +internal readonly struct GuidLayout +{ + [FieldOffset(0)] + private readonly Guid _value; + + [FieldOffset(6)] + public readonly ushort TimeHighAndVersion; + + [FieldOffset(8)] + public readonly byte ClockSequenceHighAndReserved; + + public GuidLayout(Guid value) => _value = value; +} diff --git a/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerFrameworkTests.cs b/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerFrameworkTests.cs index 05756757..3169625e 100644 --- a/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerFrameworkTests.cs +++ b/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerFrameworkTests.cs @@ -18,6 +18,8 @@ public static void Net10ExportContainsModernMembersAndNoDirectives() sourceCode.Should().Contain("using System.Numerics;"); sourceCode.Should().Contain("INumber"); + sourceCode.Should().Contain("IFloatingPointIeee754"); + sourceCode.Should().Contain("Ascii.IsValid(parameter)"); sourceCode.Should().Contain("MustBeGreaterThanOrApproximately"); sourceCode.Should().Contain("public static ReadOnlySpan MustBeEmailAddress"); sourceCode.Should().Contain("public static Span MustBeEmailAddress"); @@ -36,6 +38,8 @@ public static void NetStandardExportOmitsModernMembersAndNoDirectives() sourceCode.Should().NotContain("using System.Numerics;"); sourceCode.Should().NotContain("INumber"); + sourceCode.Should().NotContain("IFloatingPointIeee754"); + sourceCode.Should().NotContain("Ascii.IsValid(parameter)"); sourceCode.Should().NotContain("ReadOnlySpan MustBeEmailAddress"); sourceCode.Should().NotContain("Span MustBeEmailAddress"); sourceCode.Should().NotContain("Memory MustBeEmailAddress"); diff --git a/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerWhitelistTests.cs b/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerWhitelistTests.cs index f25b43b6..59a303f6 100644 --- a/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerWhitelistTests.cs +++ b/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerWhitelistTests.cs @@ -138,15 +138,71 @@ public static void PerAssertionExceptionFactoryTrimmingOnlyRemovesSelectedAssert sourceCode.Should().Contain("public static void CustomException"); } + [Fact] + public static void AdditionalAssertionsRetainPortableSupportClosures() + { + using var temporaryDirectory = new TemporaryDirectory(); + var targetFile = Path.Combine(temporaryDirectory.DirectoryPath, "AdditionalPortableAssertions.cs"); + + SourceFileMerger.CreateSingleSourceFile( + CreateOptions( + targetFile, + CreateWhitelist( + includedAssertions: + [ + new ("IsUuidVersion7", false), + new ("MustBeUuidVersion7", true), + new ("MustHaveCountIn", true), + new ("MustBeAscii", true), + new ("MustNotBeEmptyOrWhiteSpace", true), + ] + ) + ) + ); + var sourceCode = File.ReadAllText(targetFile); + + sourceCode.Should().Contain("struct GuidLayout"); + sourceCode.Should().Contain("StructLayout(LayoutKind.Explicit"); + sourceCode.Should().Contain("struct Range"); + sourceCode.Should().Contain("class EnumerableExtensions"); + sourceCode.Should().Contain("delegate Exception ReadOnlySpanExceptionFactory"); + sourceCode.Should().Contain("MustBeAscii("); + sourceCode.Should().Contain("MustNotBeEmptyOrWhiteSpace("); + } + + [Fact] + public static void FiniteWhitelistUsesTargetSpecificSurface() + { + using var temporaryDirectory = new TemporaryDirectory(); + var portableFile = Path.Combine(temporaryDirectory.DirectoryPath, "FinitePortable.cs"); + var modernFile = Path.Combine(temporaryDirectory.DirectoryPath, "FiniteModern.cs"); + var whitelist = CreateWhitelist( + includedAssertions: + [ + new ("IsFinite", true), + new ("MustBeFinite", true), + ] + ); + + SourceFileMerger.CreateSingleSourceFile(CreateOptions(portableFile, whitelist)); + SourceFileMerger.CreateSingleSourceFile( + CreateOptions(modernFile, whitelist, SourceTargetFramework.Net10_0) + ); + + File.ReadAllText(portableFile).Should().NotContain("IFloatingPointIeee754"); + File.ReadAllText(modernFile).Should().Contain("IFloatingPointIeee754"); + } + private static SourceFileMergeOptions CreateOptions( string targetFile, - AssertionWhitelist assertionWhitelist = null + AssertionWhitelist assertionWhitelist = null, + SourceTargetFramework targetFramework = SourceTargetFramework.NetStandard2_0 ) => new () { SourceFolder = SourceDirectory.FullName, TargetFile = targetFile, - TargetFramework = SourceTargetFramework.NetStandard2_0, + TargetFramework = targetFramework, IncludeVersionComment = false, AssertionWhitelist = assertionWhitelist ?? new AssertionWhitelist(), }; diff --git a/tests/Light.GuardClauses.Tests/CollectionAssertions/AdditionalSpanAndMemoryGuardsTests.cs b/tests/Light.GuardClauses.Tests/CollectionAssertions/AdditionalSpanAndMemoryGuardsTests.cs new file mode 100644 index 00000000..df77b042 --- /dev/null +++ b/tests/Light.GuardClauses.Tests/CollectionAssertions/AdditionalSpanAndMemoryGuardsTests.cs @@ -0,0 +1,167 @@ +using System; +using FluentAssertions; +using Light.GuardClauses.Exceptions; +using Xunit; + +namespace Light.GuardClauses.Tests.CollectionAssertions; + +public static class AdditionalSpanAndMemoryGuardsTests +{ + [Fact] + public static void NonEmptyGuardsReturnEveryOriginalShape() + { + var array = new[] { 1, 2 }; + var span = array.AsSpan(); + ReadOnlySpan readOnlySpan = array; + var memory = array.AsMemory(); + ReadOnlyMemory readOnlyMemory = array; + + (span.MustNotBeEmpty() == span).Should().BeTrue(); + (readOnlySpan.MustNotBeEmpty() == readOnlySpan).Should().BeTrue(); + memory.MustNotBeEmpty().Should().Be(memory); + readOnlyMemory.MustNotBeEmpty().Should().Be(readOnlyMemory); + } + + [Fact] + public static void EmptyGuardsCaptureCallerExpressions() + { + var spanAct = () => + { + var emptySpan = Span.Empty; + emptySpan.MustNotBeEmpty(); + }; + var readOnlySpanAct = () => + { + var emptyReadOnlySpan = ReadOnlySpan.Empty; + emptyReadOnlySpan.MustNotBeEmpty(); + }; + var emptyMemory = Memory.Empty; + var emptyReadOnlyMemory = ReadOnlyMemory.Empty; + + spanAct.Should().Throw().WithParameterName("emptySpan"); + readOnlySpanAct.Should().Throw().WithParameterName("emptyReadOnlySpan"); + ((Action) (() => emptyMemory.MustNotBeEmpty())).Should().Throw() + .WithParameterName(nameof(emptyMemory)); + ((Action) (() => emptyReadOnlyMemory.MustNotBeEmpty())).Should().Throw() + .WithParameterName(nameof(emptyReadOnlyMemory)); + } + + [Fact] + public static void EmptyGuardFactoriesReceiveEveryShape() + { + Test.CustomSpanException(Span.Empty, (value, factory) => value.MustNotBeEmpty(factory)); + Test.CustomSpanException(ReadOnlySpan.Empty, (value, factory) => value.MustNotBeEmpty(factory)); + Test.CustomMemoryException(Memory.Empty, (value, factory) => value.MustNotBeEmpty(factory)); + Test.CustomMemoryException(ReadOnlyMemory.Empty, (value, factory) => value.MustNotBeEmpty(factory)); + } + + [Fact] + public static void RangedLengthGuardsPreserveBoundariesAndShapes() + { + var array = new[] { 1, 2, 3 }; + var inclusive = Range.InclusiveBetween(3, 3); + var exclusive = Range.FromExclusive(3).ToInclusive(4); + var span = array.AsSpan(); + ReadOnlySpan readOnlySpan = array; + var memory = array.AsMemory(); + ReadOnlyMemory readOnlyMemory = array; + + (span.MustHaveLengthIn(inclusive) == span).Should().BeTrue(); + (readOnlySpan.MustHaveLengthIn(inclusive) == readOnlySpan).Should().BeTrue(); + memory.MustHaveLengthIn(inclusive).Should().Be(memory); + readOnlyMemory.MustHaveLengthIn(inclusive).Should().Be(readOnlyMemory); + + var act = () => memory.MustHaveLengthIn(exclusive); + act.Should().Throw() + .WithParameterName(nameof(memory)) + .WithMessage("*actually has length 3*"); + } + + [Fact] + public static void RangedLengthFactoriesReceiveEveryShape() + { + var invalidRange = Range.InclusiveBetween(2, 3); + Test.CustomSpanException(Span.Empty, invalidRange, + (value, range, factory) => value.MustHaveLengthIn(range, factory)); + Test.CustomSpanException(ReadOnlySpan.Empty, invalidRange, + (value, range, factory) => value.MustHaveLengthIn(range, factory)); + Test.CustomMemoryException(Memory.Empty, invalidRange, + (value, range, factory) => value.MustHaveLengthIn(range, factory)); + Test.CustomMemoryException(ReadOnlyMemory.Empty, invalidRange, + (value, range, factory) => value.MustHaveLengthIn(range, factory)); + } + + [Fact] + public static void MemoryExactLengthGuardsReturnOriginalShapes() + { + var memory = new[] { 1, 2 }.AsMemory(); + ReadOnlyMemory readOnlyMemory = memory; + + memory.MustHaveLength(2).Should().Be(memory); + readOnlyMemory.MustHaveLength(2).Should().Be(readOnlyMemory); + } + + [Fact] + public static void MemoryExactLengthFailuresSupportMessagesExpressionsAndFactories() + { + var memory = Memory.Empty; + ReadOnlyMemory readOnlyMemory = memory; + + ((Action) (() => memory.MustHaveLength(1, message: "custom"))) + .Should().Throw() + .WithParameterName(nameof(memory)) + .WithMessage("*custom*"); + Test.CustomMemoryException(memory, 1, (value, length, factory) => value.MustHaveLength(length, factory)); + Test.CustomMemoryException(readOnlyMemory, 1, + (value, length, factory) => value.MustHaveLength(length, factory)); + } + + [Fact] + public static void NonWhitespaceGuardsReturnEveryOriginalShape() + { + var characters = " a".ToCharArray(); + var span = characters.AsSpan(); + ReadOnlySpan readOnlySpan = characters; + var memory = characters.AsMemory(); + ReadOnlyMemory readOnlyMemory = characters; + + (span.MustNotBeEmptyOrWhiteSpace() == span).Should().BeTrue(); + (readOnlySpan.MustNotBeEmptyOrWhiteSpace() == readOnlySpan).Should().BeTrue(); + memory.MustNotBeEmptyOrWhiteSpace().Should().Be(memory); + readOnlyMemory.MustNotBeEmptyOrWhiteSpace().Should().Be(readOnlyMemory); + } + + [Fact] + public static void EmptyAndWhitespaceFailuresUseExistingStringExceptions() + { + var emptyAct = () => + { + var emptyCharacters = ReadOnlySpan.Empty; + emptyCharacters.MustNotBeEmptyOrWhiteSpace(); + }; + var whiteSpaceMemory = " \t".ToCharArray().AsMemory(); + + emptyAct.Should().Throw().WithParameterName("emptyCharacters"); + ((Action) (() => whiteSpaceMemory.MustNotBeEmptyOrWhiteSpace())) + .Should().Throw() + .WithParameterName(nameof(whiteSpaceMemory)) + .WithMessage("*actually has length 2*"); + ((Action) (() => whiteSpaceMemory.MustNotBeEmptyOrWhiteSpace(message: "custom"))) + .Should().Throw() + .WithParameterName(nameof(whiteSpaceMemory)) + .WithMessage("*custom*"); + } + + [Fact] + public static void WhitespaceFactoriesReceiveEveryShape() + { + Test.CustomSpanException(" ".ToCharArray().AsSpan(), + (value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory)); + Test.CustomSpanException((ReadOnlySpan) " ".ToCharArray(), + (value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory)); + Test.CustomMemoryException(" ".ToCharArray().AsMemory(), + (value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory)); + Test.CustomMemoryException((ReadOnlyMemory) " ".ToCharArray(), + (value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory)); + } +} diff --git a/tests/Light.GuardClauses.Tests/CollectionAssertions/MustHaveCountInTests.cs b/tests/Light.GuardClauses.Tests/CollectionAssertions/MustHaveCountInTests.cs new file mode 100644 index 00000000..f8509a99 --- /dev/null +++ b/tests/Light.GuardClauses.Tests/CollectionAssertions/MustHaveCountInTests.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using Light.GuardClauses.ExceptionFactory; +using Light.GuardClauses.Exceptions; +using Xunit; + +namespace Light.GuardClauses.Tests.CollectionAssertions; + +public static class MustHaveCountInTests +{ + [Fact] + public static void InclusiveBoundariesAreAccepted() + { + var collection = new[] { 1, 2, 3 }; + + collection.MustHaveCountIn(Range.InclusiveBetween(3, 3)).Should().BeSameAs(collection); + } + + [Theory] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(false, false)] + public static void ExclusiveBoundariesArePreserved(bool lowerInclusive, bool upperInclusive) + { + var collection = new[] { 1, 2, 3 }; + var range = new Range(3, 4, lowerInclusive, upperInclusive); + + var act = () => collection.MustHaveCountIn(range); + + if (lowerInclusive) + { + act.Should().NotThrow(); + } + else + { + act.Should().Throw(); + } + } + + [Fact] + public static void DefaultFailureReportsActualCountRangeAndExpression() + { + var collection = new[] { 1, 2, 3 }; + var range = Range.InclusiveBetween(4, 6); + + var act = () => collection.MustHaveCountIn(range); + + act.Should().Throw() + .WithParameterName(nameof(collection)) + .WithMessage("*actually has count 3*"); + } + + [Fact] + public static void ThrowHelperCapturesCollectionExpression() + { + var collection = new[] { 1, 2, 3 }; + + var act = () => Throw.CollectionCountNotInRange( + collection, + collection.Length, + Range.InclusiveBetween(4, 6) + ); + + act.Should().Throw() + .WithParameterName(nameof(collection)); + } + + [Fact] + public static void NullUsesExistingNullBehavior() + { + List collection = null; + + // ReSharper disable once ExpressionIsAlwaysNull - required for the test + var act = () => collection.MustHaveCountIn(Range.InclusiveBetween(0, 1)); + + act.Should().Throw().WithParameterName(nameof(collection)); + } + + [Fact] + public static void LazyEnumerableIsEnumeratedOnceOnSuccessAndFailure() + { + var enumerationCount = 0; + IEnumerable Values() + { + enumerationCount++; + yield return 1; + yield return 2; + } + + Values().MustHaveCountIn(Range.InclusiveBetween(2, 2)); + enumerationCount.Should().Be(1); + + var act = () => Values().MustHaveCountIn(Range.InclusiveBetween(3, 4)); + act.Should().Throw(); + enumerationCount.Should().Be(2); + } + + [Fact] + public static void FactoryOverloadEnumeratesLazyEnumerableOnceOnSuccessAndFailure() + { + var enumerationCount = 0; + IEnumerable Values() + { + enumerationCount++; + yield return 1; + yield return 2; + } + + Values().MustHaveCountIn( + Range.InclusiveBetween(2, 2), + (_, _) => new InvalidOperationException() + ); + enumerationCount.Should().Be(1); + + var act = () => Values().MustHaveCountIn( + Range.InclusiveBetween(3, 4), + (_, _) => new InvalidOperationException() + ); + act.Should().Throw(); + enumerationCount.Should().Be(2); + } + + [Fact] + public static void CustomFactoryReceivesCollectionAndRange() + { + var collection = new List { 1 }; + var range = Range.InclusiveBetween(2, 3); + + Test.CustomException(collection, range, (value, assertedRange, factory) => + value.MustHaveCountIn(assertedRange, factory)); + } + + [Fact] + public static void CustomMessage() => + Test.CustomMessage(message => + new[] { 1 }.MustHaveCountIn(Range.InclusiveBetween(2, 3), message: message)); +} diff --git a/tests/Light.GuardClauses.Tests/CommonAssertions/FiniteTests.cs b/tests/Light.GuardClauses.Tests/CommonAssertions/FiniteTests.cs new file mode 100644 index 00000000..8e53920d --- /dev/null +++ b/tests/Light.GuardClauses.Tests/CommonAssertions/FiniteTests.cs @@ -0,0 +1,86 @@ +using System; +using FluentAssertions; +using Xunit; + +namespace Light.GuardClauses.Tests.CommonAssertions; + +public static class FiniteTests +{ + [Theory] + [InlineData(0d)] + [InlineData(double.Epsilon)] + [InlineData(double.MinValue)] + [InlineData(double.MaxValue)] + public static void FiniteDoublesAreAccepted(double value) + { + value.IsFinite().Should().BeTrue(); + value.MustBeFinite().Should().Be(value); + } + + [Theory] + [InlineData(double.NaN)] + [InlineData(double.PositiveInfinity)] + [InlineData(double.NegativeInfinity)] + public static void NonFiniteDoublesAreRejected(double value) => value.IsFinite().Should().BeFalse(); + + [Theory] + [InlineData(0f)] + [InlineData(float.Epsilon)] + [InlineData(float.MinValue)] + [InlineData(float.MaxValue)] + public static void FiniteFloatsAreAccepted(float value) + { + value.IsFinite().Should().BeTrue(); + value.MustBeFinite().Should().Be(value); + } + + [Theory] + [InlineData(float.NaN)] + [InlineData(float.PositiveInfinity)] + [InlineData(float.NegativeInfinity)] + public static void NonFiniteFloatsAreRejected(float value) => value.IsFinite().Should().BeFalse(); + + [Fact] + public static void NegativeZerosAreFinite() + { + (-0d).IsFinite().Should().BeTrue(); + (-0f).IsFinite().Should().BeTrue(); + } + + [Fact] + public static void DefaultExceptionCapturesExpressionAndValue() + { + const double invalidValue = double.NaN; + + var act = () => invalidValue.MustBeFinite(); + + act.Should().Throw() + .WithParameterName(nameof(invalidValue)) + .WithMessage("*must be finite*"); + } + + [Fact] + public static void CustomMessage() => + Test.CustomMessage(message => float.PositiveInfinity.MustBeFinite(message: message)); + + [Fact] + public static void CustomFactoriesReceiveValues() + { + Test.CustomException(float.NaN, (value, factory) => value.MustBeFinite(factory)); + Test.CustomException(double.NegativeInfinity, (value, factory) => value.MustBeFinite(factory)); + } + +#if NET8_0_OR_GREATER + [Fact] + public static void GenericHalfOverloadsAreAvailable() + { + var finite = (Half) 1.5f; + var nonFinite = Half.PositiveInfinity; + + Check.IsFinite(finite).Should().BeTrue(); + Check.IsFinite(nonFinite).Should().BeFalse(); + Check.MustBeFinite(finite).Should().Be(finite); + Test.CustomException(nonFinite, (value, factory) => Check.MustBeFinite(value, factory)); + } +#endif +} diff --git a/tests/Light.GuardClauses.Tests/CommonAssertions/UuidVersion7Tests.cs b/tests/Light.GuardClauses.Tests/CommonAssertions/UuidVersion7Tests.cs new file mode 100644 index 00000000..b6898a5d --- /dev/null +++ b/tests/Light.GuardClauses.Tests/CommonAssertions/UuidVersion7Tests.cs @@ -0,0 +1,48 @@ +using System; +using FluentAssertions; +using Xunit; + +namespace Light.GuardClauses.Tests.CommonAssertions; + +public static class UuidVersion7Tests +{ + private static readonly Guid RfcExample = Guid.Parse("017f22e2-79b0-7cc3-98c4-dc0c0c07398f"); + private static readonly Guid WrongVariant = Guid.Parse("017f22e2-79b0-7cc3-18c4-dc0c0c07398f"); + + [Fact] + public static void RfcExampleIsUuidVersion7() => RfcExample.IsUuidVersion7().Should().BeTrue(); + +#if NET9_0_OR_GREATER + [Fact] + public static void FrameworkGeneratedValueIsUuidVersion7() => Guid.CreateVersion7().IsUuidVersion7().Should().BeTrue(); +#endif + + [Theory] + [MemberData(nameof(InvalidValues))] + public static void InvalidStructuresAreRejected(Guid value) => value.IsUuidVersion7().Should().BeFalse(); + + public static TheoryData InvalidValues => [Guid.Empty, Guid.NewGuid(), WrongVariant]; + + [Fact] + public static void GuardReturnsOriginalValue() => RfcExample.MustBeUuidVersion7().Should().Be(RfcExample); + + [Fact] + public static void DefaultExceptionCapturesExpression() + { + var invalidUuid = WrongVariant; + + var act = () => invalidUuid.MustBeUuidVersion7(); + + act.Should().Throw() + .WithParameterName(nameof(invalidUuid)) + .WithMessage("*RFC/IETF UUID version 7*"); + } + + [Fact] + public static void CustomMessage() => + Test.CustomMessage(message => Guid.Empty.MustBeUuidVersion7(message: message)); + + [Fact] + public static void CustomFactoryReceivesValue() => + Test.CustomException(WrongVariant, (value, factory) => value.MustBeUuidVersion7(factory)); +} diff --git a/tests/Light.GuardClauses.Tests/DateTimeAssertions/MustBeUtcTests.cs b/tests/Light.GuardClauses.Tests/DateTimeAssertions/MustBeUtcTests.cs index 938d032a..4016f6f8 100644 --- a/tests/Light.GuardClauses.Tests/DateTimeAssertions/MustBeUtcTests.cs +++ b/tests/Light.GuardClauses.Tests/DateTimeAssertions/MustBeUtcTests.cs @@ -57,4 +57,40 @@ public static void CallerArgumentExpression() act.Should().Throw() .WithParameterName(nameof(invalidDateTime)); } -} \ No newline at end of file + + [Fact] + public static void ZeroOffsetDateTimeOffsetIsReturnedUnchanged() + { + var value = new DateTimeOffset(2026, 7, 13, 10, 30, 0, TimeSpan.Zero); + + var result = value.MustBeUtc(); + + result.Should().Be(value); + result.Offset.Should().Be(TimeSpan.Zero); + } + + [Fact] + public static void NonZeroOffsetIsRejectedWithoutNormalization() + { + var nonUtcValue = new DateTimeOffset(2026, 7, 13, 12, 30, 0, TimeSpan.FromHours(2)); + + var act = () => nonUtcValue.MustBeUtc(); + + act.Should().Throw() + .WithParameterName(nameof(nonUtcValue)) + .WithMessage("*must use offset*"); + } + + [Fact] + public static void DateTimeOffsetCustomMessage() => + Test.CustomMessage(message => + DateTimeOffset.Now.MustBeUtc(message: message)); + + [Fact] + public static void DateTimeOffsetCustomFactoryReceivesValue() + { + var value = new DateTimeOffset(2026, 7, 13, 12, 30, 0, TimeSpan.FromHours(2)); + + Test.CustomException(value, (dateTimeOffset, factory) => dateTimeOffset.MustBeUtc(factory)); + } +} diff --git a/tests/Light.GuardClauses.Tests/StringAssertions/AsciiTests.cs b/tests/Light.GuardClauses.Tests/StringAssertions/AsciiTests.cs new file mode 100644 index 00000000..e4ec5701 --- /dev/null +++ b/tests/Light.GuardClauses.Tests/StringAssertions/AsciiTests.cs @@ -0,0 +1,157 @@ +using System; +using FluentAssertions; +using Xunit; + +namespace Light.GuardClauses.Tests.StringAssertions; + +public static class AsciiTests +{ + [Theory] + [InlineData('\0', true)] + [InlineData('\u007F', true)] + [InlineData('\u0080', false)] + [InlineData('é', false)] + public static void CharactersAreClassified(char value, bool expected) => value.IsAscii().Should().Be(expected); + + [Theory] + [InlineData((byte) 0, true)] + [InlineData((byte) 127, true)] + [InlineData((byte) 128, false)] + [InlineData(byte.MaxValue, false)] + public static void BytesAreClassified(byte value, bool expected) => value.IsAscii().Should().Be(expected); + + [Fact] + public static void StringsUseExpectedNullEmptyAndContentSemantics() + { + ((string) null).IsAscii().Should().BeFalse(); + string.Empty.IsAscii().Should().BeTrue(); + "ASCII\0\u007F".IsAscii().Should().BeTrue(); + "ASCIIé".IsAscii().Should().BeFalse(); + } + + [Fact] + public static void EveryBufferShapeUsesTheSameSemantics() + { + var validCharacters = "ASCII".ToCharArray(); + var invalidCharacters = "é".ToCharArray(); + var validBytes = new byte[] { 0, 65, 127 }; + var invalidBytes = new byte[] { 65, 128 }; + + validCharacters.AsSpan().IsAscii().Should().BeTrue(); + ((ReadOnlySpan) validCharacters).IsAscii().Should().BeTrue(); + validCharacters.AsMemory().IsAscii().Should().BeTrue(); + ((ReadOnlyMemory) validCharacters).IsAscii().Should().BeTrue(); + invalidCharacters.AsSpan().IsAscii().Should().BeFalse(); + ((ReadOnlySpan) invalidCharacters).IsAscii().Should().BeFalse(); + invalidCharacters.AsMemory().IsAscii().Should().BeFalse(); + ((ReadOnlyMemory) invalidCharacters).IsAscii().Should().BeFalse(); + + validBytes.AsSpan().IsAscii().Should().BeTrue(); + ((ReadOnlySpan) validBytes).IsAscii().Should().BeTrue(); + validBytes.AsMemory().IsAscii().Should().BeTrue(); + ((ReadOnlyMemory) validBytes).IsAscii().Should().BeTrue(); + invalidBytes.AsSpan().IsAscii().Should().BeFalse(); + ((ReadOnlySpan) invalidBytes).IsAscii().Should().BeFalse(); + invalidBytes.AsMemory().IsAscii().Should().BeFalse(); + ((ReadOnlyMemory) invalidBytes).IsAscii().Should().BeFalse(); + + Span.Empty.IsAscii().Should().BeTrue(); + ReadOnlySpan.Empty.IsAscii().Should().BeTrue(); + Memory.Empty.IsAscii().Should().BeTrue(); + ReadOnlyMemory.Empty.IsAscii().Should().BeTrue(); + } + + [Fact] + public static void ScalarAndStringGuardsReturnValues() + { + 'A'.MustBeAscii().Should().Be('A'); + ((byte) 127).MustBeAscii().Should().Be(127); + var text = "ASCII"; + text.MustBeAscii().Should().BeSameAs(text); + } + + [Fact] + public static void ScalarAndStringFailuresSupportDefaultMessagesFactoriesAndNull() + { + const char invalidCharacter = 'é'; + const byte invalidByte = 128; + string nullText = null; + + ((Action) (() => invalidCharacter.MustBeAscii())).Should().Throw() + .WithParameterName(nameof(invalidCharacter)); + ((Action) (() => invalidByte.MustBeAscii(message: "custom"))).Should().Throw() + .WithParameterName(nameof(invalidByte)) + .WithMessage("*custom*"); + ((Action) (() => nullText.MustBeAscii())).Should().Throw() + .WithParameterName(nameof(nullText)); + Test.CustomException(invalidCharacter, (value, factory) => value.MustBeAscii(factory)); + Test.CustomException(invalidByte, (value, factory) => value.MustBeAscii(factory)); + Test.CustomException(nullText, (value, factory) => value.MustBeAscii(factory)); + } + + [Fact] + public static void BufferGuardsReturnEveryOriginalShape() + { + var characters = "ASCII".ToCharArray(); + var bytes = new byte[] { 65, 83, 67, 73, 73 }; + var characterSpan = characters.AsSpan(); + ReadOnlySpan readOnlyCharacterSpan = characters; + var characterMemory = characters.AsMemory(); + ReadOnlyMemory readOnlyCharacterMemory = characters; + var byteSpan = bytes.AsSpan(); + ReadOnlySpan readOnlyByteSpan = bytes; + var byteMemory = bytes.AsMemory(); + ReadOnlyMemory readOnlyByteMemory = bytes; + + (characterSpan.MustBeAscii() == characterSpan).Should().BeTrue(); + (readOnlyCharacterSpan.MustBeAscii() == readOnlyCharacterSpan).Should().BeTrue(); + characterMemory.MustBeAscii().Should().Be(characterMemory); + readOnlyCharacterMemory.MustBeAscii().Should().Be(readOnlyCharacterMemory); + (byteSpan.MustBeAscii() == byteSpan).Should().BeTrue(); + (readOnlyByteSpan.MustBeAscii() == readOnlyByteSpan).Should().BeTrue(); + byteMemory.MustBeAscii().Should().Be(byteMemory); + readOnlyByteMemory.MustBeAscii().Should().Be(readOnlyByteMemory); + } + + [Fact] + public static void BufferDefaultFailuresCaptureExpressionsAndMessages() + { + var spanAct = () => + { + var invalidCharacterSpan = "é".ToCharArray().AsSpan(); + invalidCharacterSpan.MustBeAscii(); + }; + var readOnlySpanAct = () => + { + ReadOnlySpan invalidByteSpan = new byte[] { 128 }; + invalidByteSpan.MustBeAscii(message: "custom"); + }; + var invalidCharacterMemory = "é".ToCharArray().AsMemory(); + ReadOnlyMemory invalidByteMemory = new byte[] { 128 }; + + spanAct.Should().Throw().WithParameterName("invalidCharacterSpan"); + readOnlySpanAct.Should().Throw().WithParameterName("invalidByteSpan").WithMessage("*custom*"); + ((Action) (() => invalidCharacterMemory.MustBeAscii())).Should().Throw() + .WithParameterName(nameof(invalidCharacterMemory)); + ((Action) (() => invalidByteMemory.MustBeAscii())).Should().Throw() + .WithParameterName(nameof(invalidByteMemory)); + } + + [Fact] + public static void BufferFactoriesReceiveEveryShape() + { + Test.CustomSpanException("é".ToCharArray().AsSpan(), (value, factory) => value.MustBeAscii(factory)); + Test.CustomSpanException((ReadOnlySpan) "é".ToCharArray(), + (value, factory) => value.MustBeAscii(factory)); + Test.CustomMemoryException("é".ToCharArray().AsMemory(), (value, factory) => value.MustBeAscii(factory)); + Test.CustomMemoryException((ReadOnlyMemory) "é".ToCharArray(), + (value, factory) => value.MustBeAscii(factory)); + + Test.CustomSpanException(new byte[] { 128 }.AsSpan(), (value, factory) => value.MustBeAscii(factory)); + Test.CustomSpanException((ReadOnlySpan) new byte[] { 128 }, + (value, factory) => value.MustBeAscii(factory)); + Test.CustomMemoryException(new byte[] { 128 }.AsMemory(), (value, factory) => value.MustBeAscii(factory)); + Test.CustomMemoryException((ReadOnlyMemory) new byte[] { 128 }, + (value, factory) => value.MustBeAscii(factory)); + } +} diff --git a/tools/source-export/Light.GuardClauses.SourceCodeTransformation/AssertionWhitelist.cs b/tools/source-export/Light.GuardClauses.SourceCodeTransformation/AssertionWhitelist.cs index 509ab936..66efe13a 100644 --- a/tools/source-export/Light.GuardClauses.SourceCodeTransformation/AssertionWhitelist.cs +++ b/tools/source-export/Light.GuardClauses.SourceCodeTransformation/AssertionWhitelist.cs @@ -23,6 +23,8 @@ public sealed class AssertionWhitelist public AssertionEntry InvalidState { get; init; } = new(); + public AssertionEntry IsAscii { get; init; } = new(); + public AssertionEntry IsApproximately { get; init; } = new(); public AssertionEntry IsDigit { get; init; } = new(); @@ -37,6 +39,8 @@ public sealed class AssertionWhitelist public AssertionEntry IsFileExtension { get; init; } = new(); + public AssertionEntry IsFinite { get; init; } = new(); + public AssertionEntry IsGreaterThanOrApproximately { get; init; } = new(); public AssertionEntry IsIn { get; init; } = new(); @@ -75,10 +79,14 @@ public sealed class AssertionWhitelist public AssertionEntry IsTrimmedAtStart { get; init; } = new(); + public AssertionEntry IsUuidVersion7 { get; init; } = new(); + public AssertionEntry IsValidEnumValue { get; init; } = new(); public AssertionEntry IsWhiteSpace { get; init; } = new(); + public AssertionEntry MustBeAscii { get; init; } = new(); + public AssertionEntry MustBe { get; init; } = new(); public AssertionEntry MustBeAbsoluteUri { get; init; } = new(); @@ -89,6 +97,8 @@ public sealed class AssertionWhitelist public AssertionEntry MustBeFileExtension { get; init; } = new(); + public AssertionEntry MustBeFinite { get; init; } = new(); + public AssertionEntry MustBeGreaterThan { get; init; } = new(); public AssertionEntry MustBeGreaterThanOrApproximately { get; init; } = new(); @@ -139,6 +149,8 @@ public sealed class AssertionWhitelist public AssertionEntry MustBeUtc { get; init; } = new(); + public AssertionEntry MustBeUuidVersion7 { get; init; } = new(); + public AssertionEntry MustBeValidEnumValue { get; init; } = new(); public AssertionEntry MustContain { get; init; } = new(); @@ -147,6 +159,8 @@ public sealed class AssertionWhitelist public AssertionEntry MustHaveCount { get; init; } = new(); + public AssertionEntry MustHaveCountIn { get; init; } = new(); + public AssertionEntry MustHaveLength { get; init; } = new(); public AssertionEntry MustHaveLengthIn { get; init; } = new(); @@ -177,6 +191,8 @@ public sealed class AssertionWhitelist public AssertionEntry MustNotBeEmpty { get; init; } = new(); + public AssertionEntry MustNotBeEmptyOrWhiteSpace { get; init; } = new(); + public AssertionEntry MustNotBeGreaterThan { get; init; } = new(); public AssertionEntry MustNotBeGreaterThanOrEqualTo { get; init; } = new(); diff --git a/tools/source-export/Light.GuardClauses.SourceCodeTransformation/SourceFileMerger.cs b/tools/source-export/Light.GuardClauses.SourceCodeTransformation/SourceFileMerger.cs index f474e549..e55be5d3 100644 --- a/tools/source-export/Light.GuardClauses.SourceCodeTransformation/SourceFileMerger.cs +++ b/tools/source-export/Light.GuardClauses.SourceCodeTransformation/SourceFileMerger.cs @@ -68,6 +68,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Text; using System.Text.RegularExpressions; diff --git a/tools/source-export/Light.GuardClauses.SourceCodeTransformation/settings.json b/tools/source-export/Light.GuardClauses.SourceCodeTransformation/settings.json index 04dd265d..4e235a9f 100644 --- a/tools/source-export/Light.GuardClauses.SourceCodeTransformation/settings.json +++ b/tools/source-export/Light.GuardClauses.SourceCodeTransformation/settings.json @@ -25,6 +25,7 @@ "InvalidArgument": { "Include": true, "IncludeExceptionFactoryOverload": true }, "InvalidOperation": { "Include": true, "IncludeExceptionFactoryOverload": true }, "InvalidState": { "Include": true, "IncludeExceptionFactoryOverload": true }, + "IsAscii": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsApproximately": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsDigit": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsEmailAddress": { "Include": true, "IncludeExceptionFactoryOverload": true }, @@ -32,6 +33,7 @@ "IsEmptyOrWhiteSpace": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsEquivalentTypeTo": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsFileExtension": { "Include": true, "IncludeExceptionFactoryOverload": true }, + "IsFinite": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsGreaterThanOrApproximately": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsIn": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsLessThanOrApproximately": { "Include": true, "IncludeExceptionFactoryOverload": true }, @@ -51,13 +53,16 @@ "IsTrimmed": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsTrimmedAtEnd": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsTrimmedAtStart": { "Include": true, "IncludeExceptionFactoryOverload": true }, + "IsUuidVersion7": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsValidEnumValue": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsWhiteSpace": { "Include": true, "IncludeExceptionFactoryOverload": true }, + "MustBeAscii": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBe": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeAbsoluteUri": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeApproximately": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeEmailAddress": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeFileExtension": { "Include": true, "IncludeExceptionFactoryOverload": true }, + "MustBeFinite": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeGreaterThan": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeGreaterThanOrApproximately": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeGreaterThanOrEqualTo": { "Include": true, "IncludeExceptionFactoryOverload": true }, @@ -83,10 +88,12 @@ "MustBeTrimmedAtStart": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeUnspecified": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeUtc": { "Include": true, "IncludeExceptionFactoryOverload": true }, + "MustBeUuidVersion7": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeValidEnumValue": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustContain": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustEndWith": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustHaveCount": { "Include": true, "IncludeExceptionFactoryOverload": true }, + "MustHaveCountIn": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustHaveLength": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustHaveLengthIn": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustHaveMaximumCount": { "Include": true, "IncludeExceptionFactoryOverload": true }, @@ -102,6 +109,7 @@ "MustNotBeDefault": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustNotBeDefaultOrEmpty": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustNotBeEmpty": { "Include": true, "IncludeExceptionFactoryOverload": true }, + "MustNotBeEmptyOrWhiteSpace": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustNotBeGreaterThan": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustNotBeGreaterThanOrEqualTo": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustNotBeIn": { "Include": true, "IncludeExceptionFactoryOverload": true }, From bc3d07bd80ae90d93cb1b7596a25cc78a3276c91 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Mon, 13 Jul 2026 15:24:27 +0200 Subject: [PATCH 2/2] test: remove time-zone dependency for DateTimeOffsetCustomMessage Signed-off-by: Kenny Pflug --- .../DateTimeAssertions/MustBeUtcTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Light.GuardClauses.Tests/DateTimeAssertions/MustBeUtcTests.cs b/tests/Light.GuardClauses.Tests/DateTimeAssertions/MustBeUtcTests.cs index 4016f6f8..af41130b 100644 --- a/tests/Light.GuardClauses.Tests/DateTimeAssertions/MustBeUtcTests.cs +++ b/tests/Light.GuardClauses.Tests/DateTimeAssertions/MustBeUtcTests.cs @@ -84,7 +84,7 @@ public static void NonZeroOffsetIsRejectedWithoutNormalization() [Fact] public static void DateTimeOffsetCustomMessage() => Test.CustomMessage(message => - DateTimeOffset.Now.MustBeUtc(message: message)); + new DateTimeOffset(2026, 7, 13, 12, 30, 0, TimeSpan.FromHours(2)).MustBeUtc(message: message)); [Fact] public static void DateTimeOffsetCustomFactoryReceivesValue()