diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 50ff8f00..49c3cbb3 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -2,7 +2,7 @@ name: Build and Test on: push: - branches: [main, dev] + branches: [main] paths: - 'src/**' - 'tests/**' @@ -13,7 +13,7 @@ on: - 'global.json' - '.github/workflows/build-and-test.yml' pull_request: - branches: [main, dev] + branches: [main] paths: - 'src/**' - 'tests/**' @@ -24,11 +24,18 @@ on: - 'global.json' - '.github/workflows/build-and-test.yml' +permissions: + contents: read + jobs: build-and-test: + name: Build and test runs-on: ubuntu-latest + permissions: + contents: read + steps: - uses: actions/checkout@v4 - name: Setup .NET @@ -39,5 +46,71 @@ jobs: run: dotnet restore ./Light.GuardClauses.slnx - name: Build run: dotnet build ./Light.GuardClauses.slnx -c Release --no-restore + - name: Install ReportGenerator + run: dotnet tool install --global dotnet-reportgenerator-globaltool --version 5.5.10 - name: Test - run: dotnet test ./Light.GuardClauses.slnx -c Release --no-build --verbosity normal + run: > + dotnet test + --solution ./Light.GuardClauses.slnx + -c Release + --no-build + --no-restore + --results-directory artifacts/test-results + --coverage + --coverage-output-format cobertura + - name: Merge coverage + if: always() + run: > + reportgenerator + "-reports:artifacts/test-results/**/*.cobertura.xml" + "-targetdir:artifacts/coverage" + "-reporttypes:Cobertura;MarkdownSummaryGithub" + - name: Create coverage summary + uses: irongut/CodeCoverageSummary@v1.3.0 + with: + filename: artifacts/coverage/Cobertura.xml + format: markdown + output: both + badge: true + thresholds: '60 85' + - name: Store coverage summary + run: mv code-coverage-results.md artifacts/coverage/code-coverage-results.md + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results + path: artifacts/test-results + if-no-files-found: warn + - name: Upload coverage report + uses: actions/upload-artifact@v4 + if: always() + with: + name: coverage-report + path: artifacts/coverage + if-no-files-found: warn + + coverage-comment: + name: Coverage comment + runs-on: ubuntu-latest + needs: build-and-test + if: > + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository + + permissions: + contents: read + issues: write + pull-requests: write + + steps: + - name: Download coverage report + uses: actions/download-artifact@v4 + with: + name: coverage-report + path: artifacts/coverage + - name: Update pull request comment + uses: marocchino/sticky-pull-request-comment@v3.0.4 + with: + header: code-coverage + path: artifacts/coverage/code-coverage-results.md diff --git a/Directory.Packages.props b/Directory.Packages.props index 314026b2..ced114bd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,18 +9,19 @@ Keep Microsoft.CodeAnalysis.* aligned so analyzers, code fixes, and source-export tooling use one Roslyn line. --> - - + + - + + + - - + diff --git a/Light.GuardClauses.SingleFile.cs b/Light.GuardClauses.SingleFile.cs index b4d45577..ec16035d 100644 --- a/Light.GuardClauses.SingleFile.cs +++ b/Light.GuardClauses.SingleFile.cs @@ -54,6 +54,118 @@ namespace Light.GuardClauses // ReSharper disable once RedundantTypeDeclarationBody -- required for Source Code Transformation internal static class Check { + /// + /// Checks if the specified string is non-null and every character is a Unicode decimal digit. Empty strings are valid. + /// + /// The string to be checked. + /// + /// True if is non-null and every character is a Unicode decimal digit, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ContainsOnlyDigits(this string? parameter) => parameter is not null && parameter.AsSpan().ContainsOnlyDigits(); + /// + /// Checks if every character in the specified span is a Unicode decimal digit. Empty spans are valid. + /// + /// The character span to be checked. + /// + /// True if every character in is a Unicode decimal digit, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ContainsOnlyDigits(this Span parameter) => ((ReadOnlySpan)parameter).ContainsOnlyDigits(); + /// + /// Checks if every character in the specified read-only span is a Unicode decimal digit. Empty spans are valid. + /// + /// The read-only character span to be checked. + /// + /// True if every character in is a Unicode decimal digit, otherwise false. + /// + public static bool ContainsOnlyDigits(this ReadOnlySpan parameter) + { + foreach (var character in parameter) + { + if (!char.IsDigit(character)) + { + return false; + } + } + + return true; + } + + /// + /// Checks if every character in the specified memory is a Unicode decimal digit. Empty memory is valid. + /// + /// The character memory to be checked. + /// + /// True if every character in is a Unicode decimal digit, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ContainsOnlyDigits(this Memory parameter) => parameter.Span.ContainsOnlyDigits(); + /// + /// Checks if every character in the specified read-only memory is a Unicode decimal digit. Empty memory is valid. + /// + /// The read-only character memory to be checked. + /// + /// True if every character in is a Unicode decimal digit, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ContainsOnlyDigits(this ReadOnlyMemory parameter) => parameter.Span.ContainsOnlyDigits(); + /// + /// Checks if the specified string is non-null and every character is a Unicode letter or decimal digit. Empty strings are valid. + /// + /// The string to be checked. + /// + /// True if is non-null and every character is a Unicode letter or decimal digit, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ContainsOnlyLettersOrDigits(this string? parameter) => parameter is not null && parameter.AsSpan().ContainsOnlyLettersOrDigits(); + /// + /// Checks if every character in the specified span is a Unicode letter or decimal digit. Empty spans are valid. + /// + /// The character span to be checked. + /// + /// True if every character in is a Unicode letter or decimal digit, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ContainsOnlyLettersOrDigits(this Span parameter) => ((ReadOnlySpan)parameter).ContainsOnlyLettersOrDigits(); + /// + /// Checks if every character in the specified read-only span is a Unicode letter or decimal digit. Empty spans are valid. + /// + /// The read-only character span to be checked. + /// + /// True if every character in is a Unicode letter or decimal digit, otherwise false. + /// + public static bool ContainsOnlyLettersOrDigits(this ReadOnlySpan parameter) + { + foreach (var character in parameter) + { + if (!char.IsLetterOrDigit(character)) + { + return false; + } + } + + return true; + } + + /// + /// Checks if every character in the specified memory is a Unicode letter or decimal digit. Empty memory is valid. + /// + /// The character memory to be checked. + /// + /// True if every character in is a Unicode letter or decimal digit, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ContainsOnlyLettersOrDigits(this Memory parameter) => parameter.Span.ContainsOnlyLettersOrDigits(); + /// + /// Checks if every character in the specified read-only memory is a Unicode letter or decimal digit. Empty memory is valid. + /// + /// The read-only character memory to be checked. + /// + /// True if every character in is a Unicode letter or decimal digit, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ContainsOnlyLettersOrDigits(this ReadOnlyMemory parameter) => parameter.Span.ContainsOnlyLettersOrDigits(); /// /// Checks if the specified type derives from the other type. Internally, this method uses /// by default so that constructed generic types and their corresponding generic type definitions are regarded as equal. @@ -387,6 +499,129 @@ public static bool IsAscii(this ReadOnlySpan parameter) /// Checks if the read-only byte memory contains only ASCII values. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsAscii(this ReadOnlyMemory parameter) => parameter.Span.IsAscii(); + /// + /// Checks if the string is non-null and valid standard Base64. Space, tab, carriage return, and line feed are ignored. + /// Empty and whitespace-only strings are valid. + /// + /// The string to be checked. + /// True if is non-null and contains valid standard Base64, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsBase64(this string? parameter) => parameter is not null && parameter.AsSpan().IsBase64(); + /// + /// Checks if the span is valid standard Base64. Space, tab, carriage return, and line feed are ignored. + /// Empty and whitespace-only spans are valid. + /// + /// The character span to be checked. + /// True if contains valid standard Base64, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsBase64(this Span parameter) => ((ReadOnlySpan)parameter).IsBase64(); + /// + /// Checks if the read-only span is valid standard Base64. Space, tab, carriage return, and line feed are ignored. + /// Empty and whitespace-only spans are valid. + /// + /// The read-only character span to be checked. + /// True if contains valid standard Base64, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsBase64(this ReadOnlySpan parameter) + { + return IsBase64Portable(parameter); + } + + /// + /// Checks if the memory is valid standard Base64. Space, tab, carriage return, and line feed are ignored. + /// Empty and whitespace-only memory is valid. + /// + /// The character memory to be checked. + /// True if contains valid standard Base64, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsBase64(this Memory parameter) => parameter.Span.IsBase64(); + /// + /// Checks if the read-only memory is valid standard Base64. Space, tab, carriage return, and line feed are ignored. + /// Empty and whitespace-only memory is valid. + /// + /// The read-only character memory to be checked. + /// True if contains valid standard Base64, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsBase64(this ReadOnlyMemory parameter) => parameter.Span.IsBase64(); + private static bool IsBase64Portable(ReadOnlySpan parameter) + { + var nonWhiteSpaceCount = 0; + var paddingCount = 0; + var lastDataValue = 0; + foreach (var character in parameter) + { + if (character is ' ' or '\t' or '\r' or '\n') + { + continue; + } + + ++nonWhiteSpaceCount; + if (character == '=') + { + if (++paddingCount > 2) + { + return false; + } + + continue; + } + + if (paddingCount != 0 || !TryGetBase64Value(character, out lastDataValue)) + { + return false; + } + } + + if ((nonWhiteSpaceCount & 3) != 0) + { + return false; + } + + return paddingCount switch + { + 0 => true, + 1 => nonWhiteSpaceCount >= 4 && (lastDataValue & 0b11) == 0, + 2 => nonWhiteSpaceCount >= 4 && (lastDataValue & 0b1111) == 0, + _ => false, + }; + } + + private static bool TryGetBase64Value(char character, out int value) + { + if (character is >= 'A' and <= 'Z') + { + value = character - 'A'; + return true; + } + + if (character is >= 'a' and <= 'z') + { + value = character - 'a' + 26; + return true; + } + + if (character is >= '0' and <= '9') + { + value = character - '0' + 52; + return true; + } + + if (character == '+') + { + value = 62; + return true; + } + + if (character == '/') + { + value = 63; + return true; + } + + value = 0; + return false; + } + /// /// Checks if the specified character is a digit. /// @@ -609,6 +844,64 @@ public static bool IsFileExtension(this ReadOnlySpan value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsGreaterThanOrApproximately(this float value, float other) => value > other || value.IsApproximately(other); /// + /// Checks if the specified string is non-null and contains only ASCII hexadecimal characters. Empty strings are valid. + /// + /// The string to be checked. + /// + /// True if is non-null and contains only ASCII hexadecimal characters, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsHexadecimal(this string? parameter) => parameter is not null && parameter.AsSpan().IsHexadecimal(); + /// + /// Checks if the specified span contains only ASCII hexadecimal characters. Empty spans are valid. + /// + /// The character span to be checked. + /// + /// True if contains only ASCII hexadecimal characters, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsHexadecimal(this Span parameter) => ((ReadOnlySpan)parameter).IsHexadecimal(); + /// + /// Checks if the specified read-only span contains only ASCII hexadecimal characters. Empty spans are valid. + /// + /// The read-only character span to be checked. + /// + /// True if contains only ASCII hexadecimal characters, otherwise false. + /// + public static bool IsHexadecimal(this ReadOnlySpan parameter) + { + foreach (var character in parameter) + { + if (!IsAsciiHexDigit(character)) + { + return false; + } + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsAsciiHexDigit(char character) => character is >= '0' and <= '9' or >= 'A' and <= 'F' or >= 'a' and <= 'f'; + /// + /// Checks if the specified memory contains only ASCII hexadecimal characters. Empty memory is valid. + /// + /// The character memory to be checked. + /// + /// True if contains only ASCII hexadecimal characters, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsHexadecimal(this Memory parameter) => parameter.Span.IsHexadecimal(); + /// + /// Checks if the specified read-only memory contains only ASCII hexadecimal characters. Empty memory is valid. + /// + /// The read-only character memory to be checked. + /// + /// True if contains only ASCII hexadecimal characters, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsHexadecimal(this ReadOnlyMemory parameter) => parameter.Span.IsHexadecimal(); + /// /// Checks if the value is within the specified range. /// /// The comparable to be checked. @@ -675,6 +968,54 @@ public static bool IsIn([NotNull][ValidatedNotNull] this T parameter, Range char.IsLetterOrDigit(character); /// + /// Checks if the specified string is non-null and contains no Unicode uppercase character. Empty strings are valid. + /// + /// The string to be checked. + /// + /// True if is non-null and contains no Unicode uppercase character, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsLowerCase(this string? parameter) => parameter is not null && parameter.AsSpan().IsLowerCase(); + /// + /// Checks if the specified span contains no Unicode uppercase character. Empty spans are valid. + /// + /// The character span to be checked. + /// True if contains no Unicode uppercase character, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsLowerCase(this Span parameter) => ((ReadOnlySpan)parameter).IsLowerCase(); + /// + /// Checks if the specified read-only span contains no Unicode uppercase character. Empty spans are valid. + /// + /// The read-only character span to be checked. + /// True if contains no Unicode uppercase character, otherwise false. + public static bool IsLowerCase(this ReadOnlySpan parameter) + { + foreach (var character in parameter) + { + if (char.IsUpper(character)) + { + return false; + } + } + + return true; + } + + /// + /// Checks if the specified memory contains no Unicode uppercase character. Empty memory is valid. + /// + /// The character memory to be checked. + /// True if contains no Unicode uppercase character, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsLowerCase(this Memory parameter) => parameter.Span.IsLowerCase(); + /// + /// Checks if the specified read-only memory contains no Unicode uppercase character. Empty memory is valid. + /// + /// The read-only character memory to be checked. + /// True if contains no Unicode uppercase character, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsLowerCase(this ReadOnlyMemory parameter) => parameter.Span.IsLowerCase(); + /// /// Checks if the string is either "\n" or "\r\n". This is done independently of the current value of . /// /// The string to be checked. @@ -925,6 +1266,54 @@ public static bool IsSameAs([NoEnumeration] this T? parameter, [NoEnumeration [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsTrimmedAtStart(this ReadOnlySpan parameter) => parameter.Length == 0 || !parameter[0].IsWhiteSpace(); /// + /// Checks if the specified string is non-null and contains no Unicode lowercase character. Empty strings are valid. + /// + /// The string to be checked. + /// + /// True if is non-null and contains no Unicode lowercase character, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsUpperCase(this string? parameter) => parameter is not null && parameter.AsSpan().IsUpperCase(); + /// + /// Checks if the specified span contains no Unicode lowercase character. Empty spans are valid. + /// + /// The character span to be checked. + /// True if contains no Unicode lowercase character, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsUpperCase(this Span parameter) => ((ReadOnlySpan)parameter).IsUpperCase(); + /// + /// Checks if the specified read-only span contains no Unicode lowercase character. Empty spans are valid. + /// + /// The read-only character span to be checked. + /// True if contains no Unicode lowercase character, otherwise false. + public static bool IsUpperCase(this ReadOnlySpan parameter) + { + foreach (var character in parameter) + { + if (char.IsLower(character)) + { + return false; + } + } + + return true; + } + + /// + /// Checks if the specified memory contains no Unicode lowercase character. Empty memory is valid. + /// + /// The character memory to be checked. + /// True if contains no Unicode lowercase character, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsUpperCase(this Memory parameter) => parameter.Span.IsUpperCase(); + /// + /// Checks if the specified read-only memory contains no Unicode lowercase character. Empty memory is valid. + /// + /// The read-only character memory to be checked. + /// True if contains no Unicode lowercase character, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsUpperCase(this ReadOnlyMemory parameter) => parameter.Span.IsUpperCase(); + /// /// Checks if the specified GUID structurally identifies an RFC/IETF UUID version 7. /// /// The GUID to be checked. @@ -1329,7 +1718,13 @@ public static float MustBeApproximately(this float parameter, float other, float return parameter; } - /// Ensures that the character is ASCII, or otherwise throws an . + /// + /// Ensures that the character is ASCII, or otherwise throws an . + /// + /// The character to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static char MustBeAscii(this char parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) { @@ -1341,7 +1736,14 @@ public static char MustBeAscii(this char parameter, [CallerArgumentExpression("p return parameter; } - /// Ensures that the character is ASCII, or otherwise throws your custom exception. + /// + /// Ensures that the character is ASCII, or otherwise throws your custom exception. + /// + /// The character to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character. [MethodImpl(MethodImplOptions.AggressiveInlining)] [ContractAnnotation("exceptionFactory:null => halt")] public static char MustBeAscii(this char parameter, Func exceptionFactory) @@ -1354,7 +1756,13 @@ public static char MustBeAscii(this char parameter, Func except return parameter; } - /// Ensures that the byte is ASCII, or otherwise throws an . + /// + /// Ensures that the byte is ASCII, or otherwise throws an . + /// + /// The byte to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated byte. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static byte MustBeAscii(this byte parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) { @@ -1366,7 +1774,14 @@ public static byte MustBeAscii(this byte parameter, [CallerArgumentExpression("p return parameter; } - /// Ensures that the byte is ASCII, or otherwise throws your custom exception. + /// + /// Ensures that the byte is ASCII, or otherwise throws your custom exception. + /// + /// The byte to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated byte. [MethodImpl(MethodImplOptions.AggressiveInlining)] [ContractAnnotation("exceptionFactory:null => halt")] public static byte MustBeAscii(this byte parameter, Func exceptionFactory) @@ -1379,7 +1794,13 @@ public static byte MustBeAscii(this byte parameter, Func except return parameter; } - /// Ensures that the string is non-null and contains only ASCII characters. + /// + /// Ensures that the string is non-null and contains only ASCII characters, or otherwise throws a . + /// + /// The string to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated string. [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) @@ -1387,13 +1808,20 @@ public static string MustBeAscii([NotNull][ValidatedNotNull] this string? parame parameter.MustNotBeNull(parameterName, message); if (!parameter.IsAscii()) { - Throw.Argument(parameterName, message ?? $"{parameterName ?? "The string"} must contain only ASCII characters."); + Throw.InvalidStringContent(parameterName, message, "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. + /// + /// Ensures that the string is non-null and contains only ASCII characters, or otherwise throws your custom exception. + /// + /// The string to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated string. [MethodImpl(MethodImplOptions.AggressiveInlining)] [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull; exceptionFactory:null => halt")] public static string MustBeAscii([NotNull][ValidatedNotNull] this string? parameter, Func exceptionFactory) @@ -1406,7 +1834,13 @@ public static string MustBeAscii([NotNull][ValidatedNotNull] this string? parame return parameter; } - /// Ensures that the character span contains only ASCII characters. + /// + /// Ensures that the character span contains only ASCII characters, or otherwise throws a . + /// + /// The character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character span. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Span MustBeAscii(this Span parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) { @@ -1414,7 +1848,14 @@ public static Span MustBeAscii(this Span parameter, [CallerArgumentE return parameter; } - /// Ensures that the character span contains only ASCII characters, or otherwise throws your custom exception. + /// + /// Ensures that the character span contains only ASCII characters, or otherwise throws your custom exception. + /// + /// The character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character span. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Span MustBeAscii(this Span parameter, ReadOnlySpanExceptionFactory exceptionFactory) { @@ -1422,19 +1863,32 @@ public static Span MustBeAscii(this Span parameter, ReadOnlySpanExce return parameter; } - /// Ensures that the read-only character span contains only ASCII characters. + /// + /// Ensures that the read-only character span contains only ASCII characters, or otherwise throws a . + /// + /// The read-only character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character span. [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."); + Throw.InvalidStringContent(parameterName, message, "must contain only ASCII characters"); } return parameter; } - /// Ensures that the read-only character span contains only ASCII characters, or otherwise throws your custom exception. + /// + /// Ensures that the read-only character span contains only ASCII characters, or otherwise throws your custom exception. + /// + /// The read-only character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character span. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ReadOnlySpan MustBeAscii(this ReadOnlySpan parameter, ReadOnlySpanExceptionFactory exceptionFactory) { @@ -1446,7 +1900,13 @@ public static ReadOnlySpan MustBeAscii(this ReadOnlySpan parameter, return parameter; } - /// Ensures that the character memory contains only ASCII characters. + /// + /// Ensures that the character memory contains only ASCII characters, or otherwise throws a . + /// + /// The character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character memory. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Memory MustBeAscii(this Memory parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) { @@ -1454,7 +1914,14 @@ public static Memory MustBeAscii(this Memory parameter, [CallerArgum return parameter; } - /// Ensures that the character memory contains only ASCII characters, or otherwise throws your custom exception. + /// + /// Ensures that the character memory contains only ASCII characters, or otherwise throws your custom exception. + /// + /// The character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character memory. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Memory MustBeAscii(this Memory parameter, ReadOnlySpanExceptionFactory exceptionFactory) { @@ -1462,7 +1929,13 @@ public static Memory MustBeAscii(this Memory parameter, ReadOnlySpan return parameter; } - /// Ensures that the read-only character memory contains only ASCII characters. + /// + /// Ensures that the read-only character memory contains only ASCII characters, or otherwise throws a . + /// + /// The read-only character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character memory. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ReadOnlyMemory MustBeAscii(this ReadOnlyMemory parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) { @@ -1470,7 +1943,14 @@ public static ReadOnlyMemory MustBeAscii(this ReadOnlyMemory paramet return parameter; } - /// Ensures that the read-only character memory contains only ASCII characters, or otherwise throws your custom exception. + /// + /// Ensures that the read-only character memory contains only ASCII characters, or otherwise throws your custom exception. + /// + /// The read-only character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character memory. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ReadOnlyMemory MustBeAscii(this ReadOnlyMemory parameter, ReadOnlySpanExceptionFactory exceptionFactory) { @@ -1478,7 +1958,13 @@ public static ReadOnlyMemory MustBeAscii(this ReadOnlyMemory paramet return parameter; } - /// Ensures that the byte span contains only ASCII values. + /// + /// Ensures that the byte span contains only ASCII values. + /// + /// The byte span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated byte span. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Span MustBeAscii(this Span parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) { @@ -1486,7 +1972,14 @@ public static Span MustBeAscii(this Span parameter, [CallerArgumentE return parameter; } - /// Ensures that the byte span contains only ASCII values, or otherwise throws your custom exception. + /// + /// Ensures that the byte span contains only ASCII values, or otherwise throws your custom exception. + /// + /// The byte span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated byte span. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Span MustBeAscii(this Span parameter, ReadOnlySpanExceptionFactory exceptionFactory) { @@ -1494,7 +1987,13 @@ public static Span MustBeAscii(this Span parameter, ReadOnlySpanExce return parameter; } - /// Ensures that the read-only byte span contains only ASCII values. + /// + /// Ensures that the read-only byte span contains only ASCII values. + /// + /// The read-only byte span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only byte span. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ReadOnlySpan MustBeAscii(this ReadOnlySpan parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) { @@ -1506,7 +2005,14 @@ public static ReadOnlySpan MustBeAscii(this ReadOnlySpan parameter, return parameter; } - /// Ensures that the read-only byte span contains only ASCII values, or otherwise throws your custom exception. + /// + /// Ensures that the read-only byte span contains only ASCII values, or otherwise throws your custom exception. + /// + /// The read-only byte span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only byte span. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ReadOnlySpan MustBeAscii(this ReadOnlySpan parameter, ReadOnlySpanExceptionFactory exceptionFactory) { @@ -1518,7 +2024,13 @@ public static ReadOnlySpan MustBeAscii(this ReadOnlySpan parameter, return parameter; } - /// Ensures that the byte memory contains only ASCII values. + /// + /// Ensures that the byte memory contains only ASCII values. + /// + /// The byte memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated byte memory. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Memory MustBeAscii(this Memory parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) { @@ -1526,7 +2038,14 @@ public static Memory MustBeAscii(this Memory parameter, [CallerArgum return parameter; } - /// Ensures that the byte memory contains only ASCII values, or otherwise throws your custom exception. + /// + /// Ensures that the byte memory contains only ASCII values, or otherwise throws your custom exception. + /// + /// The byte memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated byte memory. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Memory MustBeAscii(this Memory parameter, ReadOnlySpanExceptionFactory exceptionFactory) { @@ -1534,7 +2053,13 @@ public static Memory MustBeAscii(this Memory parameter, ReadOnlySpan return parameter; } - /// Ensures that the read-only byte memory contains only ASCII values. + /// + /// Ensures that the read-only byte memory contains only ASCII values. + /// + /// The read-only byte memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only byte memory. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ReadOnlyMemory MustBeAscii(this ReadOnlyMemory parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) { @@ -1542,7 +2067,14 @@ public static ReadOnlyMemory MustBeAscii(this ReadOnlyMemory paramet return parameter; } - /// Ensures that the read-only byte memory contains only ASCII values, or otherwise throws your custom exception. + /// + /// Ensures that the read-only byte memory contains only ASCII values, or otherwise throws your custom exception. + /// + /// The read-only byte memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only byte memory. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ReadOnlyMemory MustBeAscii(this ReadOnlyMemory parameter, ReadOnlySpanExceptionFactory exceptionFactory) { @@ -1550,6 +2082,171 @@ public static ReadOnlyMemory MustBeAscii(this ReadOnlyMemory paramet return parameter; } + /// + /// Ensures that the string is standard Base64 with valid padding. Space, tab, carriage return, and line feed are ignored. + /// Empty and whitespace-only strings are valid. + /// + /// The string to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated string. + /// Thrown when is null. + /// Thrown when the string is not valid Base64. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static string MustBeBase64([NotNull][ValidatedNotNull] this string? parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + parameter.MustNotBeNull(parameterName, message); + if (!parameter.IsBase64()) + { + Throw.InvalidStringContent(parameterName, message, "must be valid standard Base64"); + } + + return parameter; + } + + /// + /// Ensures that the string is valid standard Base64, or otherwise throws your custom exception. + /// + /// The string to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated string. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull; exceptionFactory:null => halt")] + public static string MustBeBase64([NotNull][ValidatedNotNull] this string? parameter, Func exceptionFactory) + { + if (parameter is null || !parameter.IsBase64()) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the span is valid standard Base64. Empty and supported-whitespace-only spans are valid. + /// + /// The character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeBase64(this Span parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + ((ReadOnlySpan)parameter).MustBeBase64(parameterName, message); + return parameter; + } + + /// + /// Ensures that the span is valid standard Base64, or otherwise throws your custom exception. + /// + /// The character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeBase64(this Span parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + ((ReadOnlySpan)parameter).MustBeBase64(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only span is valid standard Base64. Empty and supported-whitespace-only spans are valid. + /// + /// The read-only character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character span. + public static ReadOnlySpan MustBeBase64(this ReadOnlySpan parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + if (!parameter.IsBase64()) + { + Throw.InvalidStringContent(parameterName, message, "must be valid standard Base64"); + } + + return parameter; + } + + /// + /// Ensures that the read-only span is valid standard Base64, or otherwise throws your custom exception. + /// + /// The read-only character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character span. + public static ReadOnlySpan MustBeBase64(this ReadOnlySpan parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + if (!parameter.IsBase64()) + { + Throw.CustomSpanException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the memory is valid standard Base64. Empty and supported-whitespace-only memory is valid. + /// + /// The character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeBase64(this Memory parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + ((ReadOnlySpan)parameter.Span).MustBeBase64(parameterName, message); + return parameter; + } + + /// + /// Ensures that the memory is valid standard Base64, or otherwise throws your custom exception. + /// + /// The character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeBase64(this Memory parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + ((ReadOnlySpan)parameter.Span).MustBeBase64(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only memory is valid standard Base64. Empty and supported-whitespace-only memory is valid. + /// + /// The read-only character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeBase64(this ReadOnlyMemory parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + parameter.Span.MustBeBase64(parameterName, message); + return parameter; + } + + /// + /// Ensures that the read-only memory is valid standard Base64, or otherwise throws your custom exception. + /// + /// The read-only character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeBase64(this ReadOnlyMemory parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + parameter.Span.MustBeBase64(exceptionFactory); + return parameter; + } + /// /// Ensures that the string is a valid email address using the default email regular expression /// defined in , or otherwise throws an . @@ -2110,37 +2807,40 @@ public static T MustBeGreaterThanOrEqualTo([NotNull][ValidatedNotNull] this T } /// - /// Ensures that the specified URI has the "http" or "https" scheme, or otherwise throws an . + /// Ensures that the string contains only ASCII hexadecimal characters. Empty strings are valid. /// - /// The URI to be checked. + /// The string to be checked. /// The name of the parameter (optional). /// The message that will be passed to the resulting exception (optional). - /// Thrown when uses a different scheme than "http" or "https". - /// Thrown when is relative and thus has no scheme. + /// The validated string. /// Thrown when is null. + /// Thrown when the string contains a non-hexadecimal character. [MethodImpl(MethodImplOptions.AggressiveInlining)] [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] - public static Uri MustBeHttpOrHttpsUrl([NotNull][ValidatedNotNull] this Uri? parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + public static string MustBeHexadecimal([NotNull][ValidatedNotNull] this string? parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) { - if (parameter.MustBeAbsoluteUri(parameterName, message).Scheme.Equals("https") == false && parameter.Scheme.Equals("http") == false) + parameter.MustNotBeNull(parameterName, message); + if (!parameter.IsHexadecimal()) { - Throw.UriMustHaveOneSchemeOf(parameter, ["https", "http"], parameterName, message); + Throw.InvalidStringContent(parameterName, message, "must contain only ASCII hexadecimal characters"); } return parameter; } /// - /// Ensures that the specified URI has the "http" or "https" scheme, or otherwise throws your custom exception. + /// Ensures that the string contains only ASCII hexadecimal characters, or otherwise throws your custom exception. /// - /// The URI to be checked. - /// The delegate that creates the exception to be thrown. is passed to this delegate. - /// Your custom exception thrown when uses a different scheme than "http" or "https", or when is a relative URI, or when is null. + /// The string to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated string. [MethodImpl(MethodImplOptions.AggressiveInlining)] - [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] - public static Uri MustBeHttpOrHttpsUrl([NotNull][ValidatedNotNull] this Uri? parameter, Func exceptionFactory) + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull; exceptionFactory:null => halt")] + public static string MustBeHexadecimal([NotNull][ValidatedNotNull] this string? parameter, Func exceptionFactory) { - if (parameter.MustBeAbsoluteUri(exceptionFactory).Scheme.Equals("https") == false && parameter.Scheme.Equals("http") == false) + if (parameter is null || !parameter.IsHexadecimal()) { Throw.CustomException(exceptionFactory, parameter); } @@ -2149,13 +2849,174 @@ public static Uri MustBeHttpOrHttpsUrl([NotNull][ValidatedNotNull] this Uri? par } /// - /// Ensures that the specified URI has the "http" scheme, or otherwise throws an . + /// Ensures that the span contains only ASCII hexadecimal characters. Empty spans are valid. /// - /// The URI to be checked. + /// The character span to be checked. /// The name of the parameter (optional). /// The message that will be passed to the resulting exception (optional). - /// Thrown when uses a different scheme than "http". - /// Thrown when is relative and thus has no scheme. + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeHexadecimal(this Span parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + ((ReadOnlySpan)parameter).MustBeHexadecimal(parameterName, message); + return parameter; + } + + /// + /// Ensures that the span contains only ASCII hexadecimal characters, or otherwise throws your custom exception. + /// + /// The character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeHexadecimal(this Span parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + ((ReadOnlySpan)parameter).MustBeHexadecimal(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only span contains only ASCII hexadecimal characters. Empty spans are valid. + /// + /// The read-only character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character span. + public static ReadOnlySpan MustBeHexadecimal(this ReadOnlySpan parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + if (!parameter.IsHexadecimal()) + { + Throw.InvalidStringContent(parameterName, message, "must contain only ASCII hexadecimal characters"); + } + + return parameter; + } + + /// + /// Ensures that the read-only span contains only ASCII hexadecimal characters, or otherwise throws your custom exception. + /// + /// The read-only character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character span. + public static ReadOnlySpan MustBeHexadecimal(this ReadOnlySpan parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + if (!parameter.IsHexadecimal()) + { + Throw.CustomSpanException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the memory contains only ASCII hexadecimal characters. Empty memory is valid. + /// + /// The character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeHexadecimal(this Memory parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + ((ReadOnlySpan)parameter.Span).MustBeHexadecimal(parameterName, message); + return parameter; + } + + /// + /// Ensures that the memory contains only ASCII hexadecimal characters, or otherwise throws your custom exception. + /// + /// The character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeHexadecimal(this Memory parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + ((ReadOnlySpan)parameter.Span).MustBeHexadecimal(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only memory contains only ASCII hexadecimal characters. Empty memory is valid. + /// + /// The read-only character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeHexadecimal(this ReadOnlyMemory parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + parameter.Span.MustBeHexadecimal(parameterName, message); + return parameter; + } + + /// + /// Ensures that the read-only memory contains only ASCII hexadecimal characters, or otherwise throws your custom exception. + /// + /// The read-only character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeHexadecimal(this ReadOnlyMemory parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + parameter.Span.MustBeHexadecimal(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the specified URI has the "http" or "https" scheme, or otherwise throws an . + /// + /// The URI to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when uses a different scheme than "http" or "https". + /// Thrown when is relative and thus has no scheme. + /// Thrown when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static Uri MustBeHttpOrHttpsUrl([NotNull][ValidatedNotNull] this Uri? parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + if (parameter.MustBeAbsoluteUri(parameterName, message).Scheme.Equals("https") == false && parameter.Scheme.Equals("http") == false) + { + Throw.UriMustHaveOneSchemeOf(parameter, ["https", "http"], parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified URI has the "http" or "https" scheme, or otherwise throws your custom exception. + /// + /// The URI to be checked. + /// The delegate that creates the exception to be thrown. is passed to this delegate. + /// Your custom exception thrown when uses a different scheme than "http" or "https", or when is a relative URI, or when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static Uri MustBeHttpOrHttpsUrl([NotNull][ValidatedNotNull] this Uri? parameter, Func exceptionFactory) + { + if (parameter.MustBeAbsoluteUri(exceptionFactory).Scheme.Equals("https") == false && parameter.Scheme.Equals("http") == false) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the specified URI has the "http" scheme, or otherwise throws an . + /// + /// The URI to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when uses a different scheme than "http". + /// Thrown when is relative and thus has no scheme. /// Thrown when is null. [MethodImpl(MethodImplOptions.AggressiveInlining)] [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] @@ -2750,6 +3611,170 @@ public static ReadOnlySpan MustBeLongerThanOrEqualTo(this ReadOnlySpan return parameter; } + /// + /// Ensures that the string contains no Unicode uppercase character. Empty and uncased strings are valid. + /// + /// The string to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated string. + /// Thrown when is null. + /// Thrown when the string contains an uppercase character. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static string MustBeLowerCase([NotNull][ValidatedNotNull] this string? parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + parameter.MustNotBeNull(parameterName, message); + if (!parameter.IsLowerCase()) + { + Throw.InvalidStringContent(parameterName, message, "must contain no Unicode uppercase characters"); + } + + return parameter; + } + + /// + /// Ensures that the string contains no Unicode uppercase character, or otherwise throws your custom exception. + /// + /// The string to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated string. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull; exceptionFactory:null => halt")] + public static string MustBeLowerCase([NotNull][ValidatedNotNull] this string? parameter, Func exceptionFactory) + { + if (parameter is null || !parameter.IsLowerCase()) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the span contains no Unicode uppercase character. Empty and uncased spans are valid. + /// + /// The character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeLowerCase(this Span parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + ((ReadOnlySpan)parameter).MustBeLowerCase(parameterName, message); + return parameter; + } + + /// + /// Ensures that the span contains no Unicode uppercase character, or otherwise throws your custom exception. + /// + /// The character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeLowerCase(this Span parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + ((ReadOnlySpan)parameter).MustBeLowerCase(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only span contains no Unicode uppercase character. Empty and uncased spans are valid. + /// + /// The read-only character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character span. + public static ReadOnlySpan MustBeLowerCase(this ReadOnlySpan parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + if (!parameter.IsLowerCase()) + { + Throw.InvalidStringContent(parameterName, message, "must contain no Unicode uppercase characters"); + } + + return parameter; + } + + /// + /// Ensures that the read-only span contains no Unicode uppercase character, or otherwise throws your custom exception. + /// + /// The read-only character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character span. + public static ReadOnlySpan MustBeLowerCase(this ReadOnlySpan parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + if (!parameter.IsLowerCase()) + { + Throw.CustomSpanException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the memory contains no Unicode uppercase character. Empty and uncased memory is valid. + /// + /// The character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeLowerCase(this Memory parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + ((ReadOnlySpan)parameter.Span).MustBeLowerCase(parameterName, message); + return parameter; + } + + /// + /// Ensures that the memory contains no Unicode uppercase character, or otherwise throws your custom exception. + /// + /// The character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeLowerCase(this Memory parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + ((ReadOnlySpan)parameter.Span).MustBeLowerCase(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only memory contains no Unicode uppercase character. Empty and uncased memory is valid. + /// + /// The read-only character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeLowerCase(this ReadOnlyMemory parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + parameter.Span.MustBeLowerCase(parameterName, message); + return parameter; + } + + /// + /// Ensures that the read-only memory contains no Unicode uppercase character, or otherwise throws your custom exception. + /// + /// The read-only character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeLowerCase(this ReadOnlyMemory parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + parameter.Span.MustBeLowerCase(exceptionFactory); + return parameter; + } + /// /// Ensures that the specified is negative (less than zero), or otherwise /// throws an . @@ -3915,12 +4940,176 @@ public static DateTime MustBeUnspecified(this DateTime parameter, Func - /// Ensures that the specified uses , or otherwise throws an . + /// Ensures that the string contains no Unicode lowercase character. Empty and uncased strings are valid. /// - /// The date time to be checked. + /// The string to be checked. /// The name of the parameter (optional). /// The message that will be passed to the resulting exception (optional). - /// Thrown when does not use . + /// The validated string. + /// Thrown when is null. + /// Thrown when the string contains a lowercase character. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static string MustBeUpperCase([NotNull][ValidatedNotNull] this string? parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + parameter.MustNotBeNull(parameterName, message); + if (!parameter.IsUpperCase()) + { + Throw.InvalidStringContent(parameterName, message, "must contain no Unicode lowercase characters"); + } + + return parameter; + } + + /// + /// Ensures that the string contains no Unicode lowercase character, or otherwise throws your custom exception. + /// + /// The string to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated string. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull; exceptionFactory:null => halt")] + public static string MustBeUpperCase([NotNull][ValidatedNotNull] this string? parameter, Func exceptionFactory) + { + if (parameter is null || !parameter.IsUpperCase()) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the span contains no Unicode lowercase character. Empty and uncased spans are valid. + /// + /// The character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeUpperCase(this Span parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + ((ReadOnlySpan)parameter).MustBeUpperCase(parameterName, message); + return parameter; + } + + /// + /// Ensures that the span contains no Unicode lowercase character, or otherwise throws your custom exception. + /// + /// The character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeUpperCase(this Span parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + ((ReadOnlySpan)parameter).MustBeUpperCase(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only span contains no Unicode lowercase character. Empty and uncased spans are valid. + /// + /// The read-only character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character span. + public static ReadOnlySpan MustBeUpperCase(this ReadOnlySpan parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + if (!parameter.IsUpperCase()) + { + Throw.InvalidStringContent(parameterName, message, "must contain no Unicode lowercase characters"); + } + + return parameter; + } + + /// + /// Ensures that the read-only span contains no Unicode lowercase character, or otherwise throws your custom exception. + /// + /// The read-only character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character span. + public static ReadOnlySpan MustBeUpperCase(this ReadOnlySpan parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + if (!parameter.IsUpperCase()) + { + Throw.CustomSpanException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the memory contains no Unicode lowercase character. Empty and uncased memory is valid. + /// + /// The character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeUpperCase(this Memory parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + ((ReadOnlySpan)parameter.Span).MustBeUpperCase(parameterName, message); + return parameter; + } + + /// + /// Ensures that the memory contains no Unicode lowercase character, or otherwise throws your custom exception. + /// + /// The character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeUpperCase(this Memory parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + ((ReadOnlySpan)parameter.Span).MustBeUpperCase(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only memory contains no Unicode lowercase character. Empty and uncased memory is valid. + /// + /// The read-only character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeUpperCase(this ReadOnlyMemory parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + parameter.Span.MustBeUpperCase(parameterName, message); + return parameter; + } + + /// + /// Ensures that the read-only memory contains no Unicode lowercase character, or otherwise throws your custom exception. + /// + /// The read-only character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeUpperCase(this ReadOnlyMemory parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + parameter.Span.MustBeUpperCase(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the specified uses , or otherwise throws an . + /// + /// The date time to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when does not use . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static DateTime MustBeUtc(this DateTime parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) { @@ -4233,123 +5422,451 @@ public static string MustContain([NotNull][ValidatedNotNull] this string? parame /// The item that must be part of the immutable array. /// The name of the parameter (optional). /// The message that will be passed to the resulting exception (optional). - /// Thrown when does not contain . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ImmutableArray MustContain(this ImmutableArray parameter, T item, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + /// Thrown when does not contain . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ImmutableArray MustContain(this ImmutableArray parameter, T item, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + if (!parameter.Contains(item)) + { + Throw.MissingItem(parameter, item, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the immutable array contains the specified item, or otherwise throws your custom exception. + /// + /// The immutable array to be checked. + /// The item that must be part of the immutable array. + /// The delegate that creates your custom exception. and are passed to this delegate. + /// Your custom exception thrown when does not contain . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ImmutableArray MustContain(this ImmutableArray parameter, T item, Func, T, Exception> exceptionFactory) + { + if (!parameter.Contains(item)) + { + Throw.CustomException(exceptionFactory, parameter, item); + } + + return parameter; + } + + /// + /// Ensures that the dictionary contains the specified key, or otherwise throws a . + /// The check is performed via , so the dictionary is + /// never enumerated and its key comparer is respected. + /// + /// The dictionary to be checked. + /// The key that must be present in the dictionary. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when does not contain . + /// Thrown when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static IReadOnlyDictionary MustContainKey([NotNull][ValidatedNotNull] this IReadOnlyDictionary? parameter, TKey key, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + if (!parameter.MustNotBeNull(parameterName, message).ContainsKey(key)) + { + Throw.MissingKey(parameter, key, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the dictionary contains the specified key, or otherwise throws your custom exception. + /// The check is performed via , so the dictionary is + /// never enumerated and its key comparer is respected. + /// + /// The dictionary to be checked. + /// The key that must be present in the dictionary. + /// The delegate that creates your custom exception. and are passed to this delegate. + /// Your custom exception thrown when does not contain , or when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static IReadOnlyDictionary MustContainKey([NotNull][ValidatedNotNull] this IReadOnlyDictionary? parameter, TKey key, Func?, TKey, Exception> exceptionFactory) + { + if (parameter is null || !parameter.ContainsKey(key)) + { + Throw.CustomException(exceptionFactory, parameter, key); + } + + return parameter; + } + + /// + /// Ensures that the dictionary contains the specified key, or otherwise throws a . + /// The check is performed via , so the dictionary is + /// never enumerated and its key comparer is respected. + /// + /// The dictionary to be checked. + /// The key that must be present in the dictionary. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when does not contain . + /// Thrown when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static Dictionary MustContainKey([NotNull][ValidatedNotNull] this Dictionary? parameter, TKey key, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + where TKey : notnull + { + if (!parameter.MustNotBeNull(parameterName, message).ContainsKey(key)) + { + Throw.MissingKey(parameter, key, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the dictionary contains the specified key, or otherwise throws your custom exception. + /// The check is performed via , so the dictionary is + /// never enumerated and its key comparer is respected. + /// + /// The dictionary to be checked. + /// The key that must be present in the dictionary. + /// The delegate that creates your custom exception. and are passed to this delegate. + /// Your custom exception thrown when does not contain , or when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static Dictionary MustContainKey([NotNull][ValidatedNotNull] this Dictionary? parameter, TKey key, Func?, TKey, Exception> exceptionFactory) + where TKey : notnull + { + if (parameter is null || !parameter.ContainsKey(key)) + { + Throw.CustomException(exceptionFactory, parameter, key); + } + + return parameter; + } + + /// + /// Ensures that the string contains only Unicode decimal digits. Empty strings are valid. + /// + /// The string to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated string. + /// Thrown when is null. + /// Thrown when the string contains a non-digit character. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static string MustContainOnlyDigits([NotNull][ValidatedNotNull] this string? parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + parameter.MustNotBeNull(parameterName, message); + if (!parameter.ContainsOnlyDigits()) + { + Throw.InvalidStringContent(parameterName, message, "must contain only Unicode decimal digits"); + } + + return parameter; + } + + /// + /// Ensures that the string contains only Unicode decimal digits, or otherwise throws your custom exception. + /// + /// The string to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated string. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull; exceptionFactory:null => halt")] + public static string MustContainOnlyDigits([NotNull][ValidatedNotNull] this string? parameter, Func exceptionFactory) + { + if (parameter is null || !parameter.ContainsOnlyDigits()) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the span contains only Unicode decimal digits. Empty spans are valid. + /// + /// The character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustContainOnlyDigits(this Span parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + ((ReadOnlySpan)parameter).MustContainOnlyDigits(parameterName, message); + return parameter; + } + + /// + /// Ensures that the span contains only Unicode decimal digits, or otherwise throws your custom exception. + /// + /// The character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustContainOnlyDigits(this Span parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + ((ReadOnlySpan)parameter).MustContainOnlyDigits(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only span contains only Unicode decimal digits. Empty spans are valid. + /// + /// The read-only character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character span. + public static ReadOnlySpan MustContainOnlyDigits(this ReadOnlySpan parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + if (!parameter.ContainsOnlyDigits()) + { + Throw.InvalidStringContent(parameterName, message, "must contain only Unicode decimal digits"); + } + + return parameter; + } + + /// + /// Ensures that the read-only span contains only Unicode decimal digits, or otherwise throws your custom exception. + /// + /// The read-only character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character span. + public static ReadOnlySpan MustContainOnlyDigits(this ReadOnlySpan parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + if (!parameter.ContainsOnlyDigits()) + { + Throw.CustomSpanException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the memory contains only Unicode decimal digits. Empty memory is valid. + /// + /// The character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustContainOnlyDigits(this Memory parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + ((ReadOnlySpan)parameter.Span).MustContainOnlyDigits(parameterName, message); + return parameter; + } + + /// + /// Ensures that the memory contains only Unicode decimal digits, or otherwise throws your custom exception. + /// + /// The character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustContainOnlyDigits(this Memory parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + ((ReadOnlySpan)parameter.Span).MustContainOnlyDigits(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only memory contains only Unicode decimal digits. Empty memory is valid. + /// + /// The read-only character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustContainOnlyDigits(this ReadOnlyMemory parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + parameter.Span.MustContainOnlyDigits(parameterName, message); + return parameter; + } + + /// + /// Ensures that the read-only memory contains only Unicode decimal digits, or otherwise throws your custom exception. + /// + /// The read-only character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustContainOnlyDigits(this ReadOnlyMemory parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + parameter.Span.MustContainOnlyDigits(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the string contains only Unicode letters or decimal digits. Empty strings are valid. + /// + /// The string to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated string. + /// Thrown when is null. + /// Thrown when the string contains another character. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static string MustContainOnlyLettersOrDigits([NotNull][ValidatedNotNull] this string? parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + parameter.MustNotBeNull(parameterName, message); + if (!parameter.ContainsOnlyLettersOrDigits()) + { + Throw.InvalidStringContent(parameterName, message, "must contain only Unicode letters or decimal digits"); + } + + return parameter; + } + + /// + /// Ensures that the string contains only Unicode letters or decimal digits, or otherwise throws your custom exception. + /// + /// The string to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated string. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull; exceptionFactory:null => halt")] + public static string MustContainOnlyLettersOrDigits([NotNull][ValidatedNotNull] this string? parameter, Func exceptionFactory) + { + if (parameter is null || !parameter.ContainsOnlyLettersOrDigits()) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the span contains only Unicode letters or decimal digits. Empty spans are valid. + /// + /// The character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustContainOnlyLettersOrDigits(this Span parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + ((ReadOnlySpan)parameter).MustContainOnlyLettersOrDigits(parameterName, message); + return parameter; + } + + /// + /// Ensures that the span contains only Unicode letters or decimal digits, or otherwise throws your custom exception. + /// + /// The character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustContainOnlyLettersOrDigits(this Span parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + ((ReadOnlySpan)parameter).MustContainOnlyLettersOrDigits(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only span contains only Unicode letters or decimal digits. Empty spans are valid. + /// + /// The read-only character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character span. + public static ReadOnlySpan MustContainOnlyLettersOrDigits(this ReadOnlySpan parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) { - if (!parameter.Contains(item)) + if (!parameter.ContainsOnlyLettersOrDigits()) { - Throw.MissingItem(parameter, item, parameterName, message); + Throw.InvalidStringContent(parameterName, message, "must contain only Unicode letters or decimal digits"); } return parameter; } /// - /// Ensures that the immutable array contains the specified item, or otherwise throws your custom exception. + /// Ensures that the read-only span contains only Unicode letters or decimal digits, or otherwise throws your custom exception. /// - /// The immutable array to be checked. - /// The item that must be part of the immutable array. - /// The delegate that creates your custom exception. and are passed to this delegate. - /// Your custom exception thrown when does not contain . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ImmutableArray MustContain(this ImmutableArray parameter, T item, Func, T, Exception> exceptionFactory) + /// The read-only character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character span. + public static ReadOnlySpan MustContainOnlyLettersOrDigits(this ReadOnlySpan parameter, ReadOnlySpanExceptionFactory exceptionFactory) { - if (!parameter.Contains(item)) + if (!parameter.ContainsOnlyLettersOrDigits()) { - Throw.CustomException(exceptionFactory, parameter, item); + Throw.CustomSpanException(exceptionFactory, parameter); } return parameter; } /// - /// Ensures that the dictionary contains the specified key, or otherwise throws a . - /// The check is performed via , so the dictionary is - /// never enumerated and its key comparer is respected. + /// Ensures that the memory contains only Unicode letters or decimal digits. Empty memory is valid. /// - /// The dictionary to be checked. - /// The key that must be present in the dictionary. + /// The character memory to be checked. /// The name of the parameter (optional). /// The message that will be passed to the resulting exception (optional). - /// Thrown when does not contain . - /// Thrown when is null. + /// The validated character memory. [MethodImpl(MethodImplOptions.AggressiveInlining)] - [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] - public static IReadOnlyDictionary MustContainKey([NotNull][ValidatedNotNull] this IReadOnlyDictionary? parameter, TKey key, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + public static Memory MustContainOnlyLettersOrDigits(this Memory parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) { - if (!parameter.MustNotBeNull(parameterName, message).ContainsKey(key)) - { - Throw.MissingKey(parameter, key, parameterName, message); - } - + ((ReadOnlySpan)parameter.Span).MustContainOnlyLettersOrDigits(parameterName, message); return parameter; } /// - /// Ensures that the dictionary contains the specified key, or otherwise throws your custom exception. - /// The check is performed via , so the dictionary is - /// never enumerated and its key comparer is respected. + /// Ensures that the memory contains only Unicode letters or decimal digits, or otherwise throws your custom exception. /// - /// The dictionary to be checked. - /// The key that must be present in the dictionary. - /// The delegate that creates your custom exception. and are passed to this delegate. - /// Your custom exception thrown when does not contain , or when is null. + /// The character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character memory. [MethodImpl(MethodImplOptions.AggressiveInlining)] - [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] - public static IReadOnlyDictionary MustContainKey([NotNull][ValidatedNotNull] this IReadOnlyDictionary? parameter, TKey key, Func?, TKey, Exception> exceptionFactory) + public static Memory MustContainOnlyLettersOrDigits(this Memory parameter, ReadOnlySpanExceptionFactory exceptionFactory) { - if (parameter is null || !parameter.ContainsKey(key)) - { - Throw.CustomException(exceptionFactory, parameter, key); - } - + ((ReadOnlySpan)parameter.Span).MustContainOnlyLettersOrDigits(exceptionFactory); return parameter; } /// - /// Ensures that the dictionary contains the specified key, or otherwise throws a . - /// The check is performed via , so the dictionary is - /// never enumerated and its key comparer is respected. + /// Ensures that the read-only memory contains only Unicode letters or decimal digits. Empty memory is valid. /// - /// The dictionary to be checked. - /// The key that must be present in the dictionary. + /// The read-only character memory to be checked. /// The name of the parameter (optional). /// The message that will be passed to the resulting exception (optional). - /// Thrown when does not contain . - /// Thrown when is null. + /// The validated read-only character memory. [MethodImpl(MethodImplOptions.AggressiveInlining)] - [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] - public static Dictionary MustContainKey([NotNull][ValidatedNotNull] this Dictionary? parameter, TKey key, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) - where TKey : notnull + public static ReadOnlyMemory MustContainOnlyLettersOrDigits(this ReadOnlyMemory parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) { - if (!parameter.MustNotBeNull(parameterName, message).ContainsKey(key)) - { - Throw.MissingKey(parameter, key, parameterName, message); - } - + parameter.Span.MustContainOnlyLettersOrDigits(parameterName, message); return parameter; } /// - /// Ensures that the dictionary contains the specified key, or otherwise throws your custom exception. - /// The check is performed via , so the dictionary is - /// never enumerated and its key comparer is respected. + /// Ensures that the read-only memory contains only Unicode letters or decimal digits, or otherwise throws your custom exception. /// - /// The dictionary to be checked. - /// The key that must be present in the dictionary. - /// The delegate that creates your custom exception. and are passed to this delegate. - /// Your custom exception thrown when does not contain , or when is null. + /// The read-only character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character memory. [MethodImpl(MethodImplOptions.AggressiveInlining)] - [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] - public static Dictionary MustContainKey([NotNull][ValidatedNotNull] this Dictionary? parameter, TKey key, Func?, TKey, Exception> exceptionFactory) - where TKey : notnull + public static ReadOnlyMemory MustContainOnlyLettersOrDigits(this ReadOnlyMemory parameter, ReadOnlySpanExceptionFactory exceptionFactory) { - if (parameter is null || !parameter.ContainsKey(key)) - { - Throw.CustomException(exceptionFactory, parameter, key); - } - + parameter.Span.MustContainOnlyLettersOrDigits(exceptionFactory); return parameter; } @@ -7874,29 +9391,14 @@ public static TCollection MustNotContainNullOrWhiteSpace([NotNull][ return parameter; } - private static int FindNullOrWhiteSpaceItem(IEnumerable parameter, out string? invalidItem) + private static int FindNullOrWhiteSpaceItem(IEnumerable parameter, out string? invalidItem) => parameter switch + { + IList list => FindNullOrWhiteSpaceItem(list, out invalidItem), + IReadOnlyList readOnlyList => FindNullOrWhiteSpaceItem(readOnlyList, out invalidItem), + _ => FindNullOrWhiteSpaceItemViaForeach(parameter, out invalidItem), + }; + private static int FindNullOrWhiteSpaceItemViaForeach(IEnumerable parameter, out string? invalidItem) { - if (parameter is IList list) - { - return FindNullOrWhiteSpaceItem(list, out invalidItem); - } - - if (parameter is IReadOnlyList readOnlyList) - { - for (var position = 0; position < readOnlyList.Count; ++position) - { - var item = readOnlyList[position]; - if (item.IsNullOrWhiteSpace()) - { - invalidItem = item; - return position; - } - } - - invalidItem = null; - return -1; - } - var currentPosition = 0; foreach (var item in parameter) { @@ -7929,6 +9431,200 @@ private static int FindNullOrWhiteSpaceItem(IList list, out string? inv return -1; } + private static int FindNullOrWhiteSpaceItem(IReadOnlyList list, out string? invalidItem) + { + for (var position = 0; position < list.Count; ++position) + { + var item = list[position]; + if (item.IsNullOrWhiteSpace()) + { + invalidItem = item; + return position; + } + } + + invalidItem = null; + return -1; + } + + /// + /// Ensures that the string contains no Unicode whitespace character. Empty strings are valid. + /// + /// The string to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated string. + /// Thrown when is null. + /// Thrown when the string contains a whitespace character. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static string MustNotContainWhiteSpace([NotNull][ValidatedNotNull] this string? parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + parameter.MustNotBeNull(parameterName, message); + parameter.AsSpan().MustNotContainWhiteSpace(parameterName, message); + return parameter; + } + + /// + /// Ensures that the string contains no Unicode whitespace character, or otherwise throws your custom exception. + /// + /// The string to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated string. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull; exceptionFactory:null => halt")] + public static string MustNotContainWhiteSpace([NotNull][ValidatedNotNull] this string? parameter, Func exceptionFactory) + { + if (parameter is null) + { + Throw.CustomException(exceptionFactory, parameter); + } + + if (ContainsWhiteSpace(parameter.AsSpan())) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the span contains no Unicode whitespace character. Empty spans are valid. + /// + /// The character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustNotContainWhiteSpace(this Span parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + ((ReadOnlySpan)parameter).MustNotContainWhiteSpace(parameterName, message); + return parameter; + } + + /// + /// Ensures that the span contains no Unicode whitespace character, or otherwise throws your custom exception. + /// + /// The character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustNotContainWhiteSpace(this Span parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + ((ReadOnlySpan)parameter).MustNotContainWhiteSpace(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only span contains no Unicode whitespace character. Empty spans are valid. + /// + /// The read-only character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character span. + public static ReadOnlySpan MustNotContainWhiteSpace(this ReadOnlySpan parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + if (ContainsWhiteSpace(parameter)) + { + Throw.InvalidStringContent(parameterName, message, "must contain no Unicode whitespace characters"); + } + + return parameter; + } + + /// + /// Ensures that the read-only span contains no Unicode whitespace character, or otherwise throws your custom exception. + /// + /// The read-only character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character span. + public static ReadOnlySpan MustNotContainWhiteSpace(this ReadOnlySpan parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + if (ContainsWhiteSpace(parameter)) + { + Throw.CustomSpanException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the memory contains no Unicode whitespace character. Empty memory is valid. + /// + /// The character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustNotContainWhiteSpace(this Memory parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + ((ReadOnlySpan)parameter.Span).MustNotContainWhiteSpace(parameterName, message); + return parameter; + } + + /// + /// Ensures that the memory contains no Unicode whitespace character, or otherwise throws your custom exception. + /// + /// The character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustNotContainWhiteSpace(this Memory parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + ((ReadOnlySpan)parameter.Span).MustNotContainWhiteSpace(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only memory contains no Unicode whitespace character. Empty memory is valid. + /// + /// The read-only character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustNotContainWhiteSpace(this ReadOnlyMemory parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, string? message = null) + { + parameter.Span.MustNotContainWhiteSpace(parameterName, message); + return parameter; + } + + /// + /// Ensures that the read-only memory contains no Unicode whitespace character, or otherwise throws your custom exception. + /// + /// The read-only character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustNotContainWhiteSpace(this ReadOnlyMemory parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + parameter.Span.MustNotContainWhiteSpace(exceptionFactory); + return parameter; + } + + private static bool ContainsWhiteSpace(ReadOnlySpan parameter) + { + foreach (var character in parameter) + { + if (char.IsWhiteSpace(character)) + { + return true; + } + } + + return false; + } + /// /// Ensures that the string does not end with the specified value, or otherwise throws a . /// @@ -9695,6 +11391,15 @@ public static void InvalidMinimumImmutableArrayLength(ImmutableArray param [DoesNotReturn] public static void InvalidState(string? message = null) => throw new InvalidStateException(message); /// + /// Throws a for character content that violates a required invariant. + /// + /// The name of the parameter that contains the invalid string (optional). + /// The message that will be passed to the exception (optional). + /// A description of the requirement that the string violates. + [ContractAnnotation("=> halt")] + [DoesNotReturn] + public static void InvalidStringContent(string? parameterName, string? message, string requirement) => throw new StringException(parameterName, message ?? $"{parameterName ?? "The string"} {requirement}."); + /// /// Throws the default indicating that a reference cannot be downcast, using the /// optional parameter name and message. /// diff --git a/ai-plans/0153-string-inspection-guards.md b/ai-plans/0153-string-inspection-guards.md new file mode 100644 index 00000000..07417a53 --- /dev/null +++ b/ai-plans/0153-string-inspection-guards.md @@ -0,0 +1,62 @@ +# String Inspection Guards + +## Rationale + +Identifiers, codes, tokens, hashes, and other normalized strings commonly have invariants about every character they contain. Callers currently express those invariants with repeated loops, regular expressions, or parsing APIs, obscuring intent and often introducing avoidable allocations. Add named predicates and fluent guards for digit, alphanumeric, whitespace, casing, Base64, and hexadecimal content. + +The additions must mirror the character-oriented `IsAscii` overload shapes, behave identically across all package targets, remain allocation-free per invocation, use optimized .NET 10 framework primitives where their semantics match, and participate in the customizable single-file source distribution. + +## Acceptance Criteria + +- [x] `ContainsOnlyDigits` / `MustContainOnlyDigits`, `ContainsOnlyLettersOrDigits` / `MustContainOnlyLettersOrDigits`, `IsUpperCase` / `MustBeUpperCase`, `IsLowerCase` / `MustBeLowerCase`, `IsBase64` / `MustBeBase64`, and `IsHexadecimal` / `MustBeHexadecimal` are available for `string`, `Span`, `ReadOnlySpan`, `Memory`, and `ReadOnlyMemory` on .NET Standard 2.0, .NET Standard 2.1, and .NET 10. +- [x] `MustNotContainWhiteSpace` is available for the same five receiver shapes and target frameworks. +- [x] Digit and alphanumeric scans use the same Unicode character-classification semantics as the existing `IsDigit(char)` and `IsLetterOrDigit(char)` predicates; hexadecimal accepts only ASCII `0`-`9`, `A`-`F`, and `a`-`f`. +- [x] `IsUpperCase` means that the input contains no character classified as lowercase, and `IsLowerCase` means that it contains no character classified as uppercase. Uncased letters, digits, punctuation, symbols, and whitespace do not by themselves violate either predicate. +- [x] `MustNotContainWhiteSpace` rejects every character recognized by the existing `IsWhiteSpace(char)` predicate, including non-ASCII Unicode whitespace, and accepts an empty input. +- [x] `IsBase64` validates standard, decodable Base64 without decoding it: it accepts the `A`-`Z`, `a`-`z`, `0`-`9`, `+`, and `/` alphabet; enforces valid quartet and trailing-padding structure; permits only space, tab, carriage return, and line feed as ignorable whitespace anywhere; and rejects Base64Url-only characters and malformed input. +- [x] Empty strings and buffers satisfy every predicate and throwing guard. A null string returns `false` from every predicate and causes every default throwing guard to throw `ArgumentNullException`. +- [x] Every throwing overload returns the original successfully validated receiver in its exact shape, captures the guarded expression for default exceptions, accepts an optional custom message, and has a custom-exception-factory counterpart consistent with the existing string, span, and memory APIs. Invalid non-null content throws `StringException` by default. +- [x] The existing `MustBeAscii` overloads for `string`, `Span`, `ReadOnlySpan`, `Memory`, and `ReadOnlyMemory` also throw `StringException` for invalid non-null content; the scalar `char` and `byte` overloads and all byte-buffer overloads retain `ArgumentException` because they do not validate string content. +- [x] All scans short-circuit once the outcome is known, use O(n) time and constant additional space, and perform no per-invocation allocation, copying, regular-expression evaluation, LINQ enumeration, normalization, decoding, or case conversion. +- [x] The .NET 10 Base64 implementation uses `Base64.IsValid`; other .NET 10 implementations use vectorized or otherwise optimized framework span APIs only where they preserve the defined semantics and benchmarks demonstrate an advantage over the portable scalar loop. Portable assets remain allocation-free and observably identical. +- [x] Automated tests cover null and empty values; valid and invalid content at the beginning, middle, and end; every receiver and fluent return shape; default `StringException` contracts and custom messages; caller argument expressions; custom factories; Unicode digits, letters, casing, and whitespace; uncased content; both hexadecimal cases; representative valid and malformed Base64 alphabet, length, padding, whitespace, and Base64Url cases; and the revised character-sequence versus retained scalar/byte `MustBeAscii` exception behavior. +- [x] Semantic conformance tests exhaustively classify every UTF-16 code-unit value for the digit, alphanumeric, whitespace, casing, and hexadecimal contracts, and directly compare the actual portable Base64 validator with .NET 10 `Base64.IsValid` over a deterministic corpus of valid and malformed inputs. +- [x] Microbenchmarks compare representative short and long successful and failing scans with equivalent imperative checks, include ASCII and mixed Unicode scenarios for proposed modern fast paths, report allocations, confirm `0 B/op` for predicates and successful guards after any one-time framework lookup initialization, and provide the evidence for retaining any fast path that can otherwise add a second scan. +- [x] The source-export whitelist catalog and committed settings contain every new assertion family, and focused source-export tests verify guard-to-predicate/helper reachability, framework-conditional code, and exception-factory trimming. +- [x] The committed .NET Standard 2.0 single-file distribution is regenerated with the portable implementations, and generated source validates for both .NET Standard 2.0 and .NET 10. +- [x] XML documentation and the assertion overview describe the supported receiver shapes, null and empty behavior, Unicode versus ASCII classification, casing definition, Base64 whitespace and padding rules, allocation behavior, and target-specific acceleration. +- [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 `Check.IsAscii.cs` and `Check.MustBeAscii.cs` organization and delegation model. Each predicate family should have one `ReadOnlySpan` implementation; `string`, mutable span, and both memory shapes delegate to it without copying. Each guard should likewise centralize validation in its read-only span overload, then return the original mutable span or memory value. String guards retain nullable-flow annotations and first apply the established null behavior. Use `ReadOnlySpanExceptionFactory` for span-backed custom factories, consistent with `MustBeAscii`. + +The requested families are: + +- `ContainsOnlyDigits` and `MustContainOnlyDigits`; +- `ContainsOnlyLettersOrDigits` and `MustContainOnlyLettersOrDigits`; +- `MustNotContainWhiteSpace` (throwing-only); +- `IsUpperCase` and `MustBeUpperCase`; +- `IsLowerCase` and `MustBeLowerCase`; +- `IsBase64` and `MustBeBase64`; and +- `IsHexadecimal` and `MustBeHexadecimal`. + +Digit and alphanumeric semantics deliberately compose with the library's existing scalar predicates rather than introducing an ASCII-only interpretation under an unqualified name. Classify UTF-16 code units with `char.IsDigit` / `char.IsLetterOrDigit` (or the existing extension predicates), including their Unicode behavior. A .NET 10 vectorized ASCII allowed-set or range search may accept the common all-ASCII case quickly; if it encounters a non-ASCII code unit, continue with the Unicode scalar classifier so the result remains identical to portable targets. Retain such a fast path only when the required benchmarks show that its additional complexity and possible second scan improve the intended workloads. Do not silently reject Unicode decimal digits or letters in the optimized path. + +For casing, scan for the prohibited category rather than requiring at least one cased letter: `IsUpperCase` fails on any code unit for which `char.IsLower` is true, while `IsLowerCase` fails on any code unit for which `char.IsUpper` is true. This intentionally makes empty and entirely uncased inputs satisfy both predicates. Use invariant Unicode classification only; culture-sensitive conversion or comparison would make guard behavior depend on the current culture and would allocate or transform the input. + +Whitespace follows `char.IsWhiteSpace`, matching the library's existing scalar predicate. Do not implement it with an ASCII-only search or with `string.IsNullOrWhiteSpace`, because the former changes Unicode behavior and the latter answers whether all characters are whitespace rather than whether any character is whitespace. A Boolean `DoesNotContainWhiteSpace` counterpart is out of scope because it was not requested. + +Hexadecimal is a character-set scan, not a numeric parse: length, evenness, prefixes such as `0x` and `#`, sign, separators, and numeric range are irrelevant. On .NET 10, use `char.IsAsciiHexDigit` or a benchmark-supported optimized span allowed-set; portable code checks the same three ASCII ranges directly. The per-call path must not construct an allowed-character collection. + +Base64 is a structural scan but does not produce decoded bytes. Its contract must match `.NET 10` `System.Buffers.Text.Base64.IsValid(ReadOnlySpan)`, including vacuous validity for empty or whitespace-only input and the exact ignorable whitespace set (`' '`, `'\t'`, `'\r'`, `'\n'`). Portable targets need a small stateful validator for alphabet, effective non-whitespace length, terminal `=` placement, and padding count; do not use `Convert.FromBase64String`, temporary byte buffers, or regexes. Keep the actual portable implementation directly testable against `Base64.IsValid` under .NET 10 so the differential test does not validate a duplicated test-side algorithm. Use a deterministic corpus that varies effective length, alphabet classes, whitespace placement, padding placement and count, and Base64Url-only characters. Keep this distinct from Base64Url, and do not add decoded-length outputs or byte-span overloads as part of this issue. + +Use framework conditionals already understood by the source exporter. Suitable .NET 10 building blocks include `MemoryExtensions.ContainsAnyExceptInRange`, reusable static `SearchValues` instances with `ContainsAnyExcept` / `IndexOfAnyExcept`, and `Base64.IsValid`; choose the smallest primitive that preserves the defined semantics, and require benchmark evidence before keeping a non-Base64 fast path that can perform extra work on fallback. Verify the generated modern source contains the selected fast paths while the portable output contains no unavailable APIs. One-time static lookup construction is acceptable, but steady-state predicate and successful-guard invocations must allocate nothing and remain Native AOT compatible. + +Exhaustive single-code-unit tests are practical because the `char` domain contains only 65,536 values. Compare each value with the defining `char.IsDigit`, `char.IsLetterOrDigit`, `char.IsWhiteSpace`, `char.IsLower` / `char.IsUpper`, or ASCII hexadecimal classification as appropriate, then retain multi-character boundary and mixed-content tests to verify scan composition and short-circuiting. + +Default non-null content failures use the existing `StringException`, preserving the library's string-specific exception hierarchy while remaining catch-compatible with `ArgumentException`. Use focused throw helpers with concise messages naming the guarded expression and required invariant without embedding the full input, which may be large or sensitive. Null strings continue to use `ArgumentNullException`, and custom factories receive null or invalid values exactly as `MustBeAscii` does. Existing specialized exceptions such as `SubstringException`, `StringDoesNotMatchException`, and `WhiteSpaceStringException` do not match these contracts, and no new public exception type is required. + +Align the unpublished `MustBeAscii` character-sequence API with this contract: invalid `string`, `Span`, `ReadOnlySpan`, `Memory`, and `ReadOnlyMemory` values throw `StringException`. Keep `ArgumentException` for `char`, `byte`, `Span`, `ReadOnlySpan`, `Memory`, and `ReadOnlyMemory`, whose receivers are scalars or byte buffers rather than strings. Preserve `ArgumentNullException` for a null string and all existing custom-factory behavior. Update its XML documentation, tests, throw-helper reachability, and generated single-file output together with the new families. + +Add every family to `AssertionWhitelist` and `settings.json`, extend source-export reachability and trimming coverage, regenerate `Light.GuardClauses.SingleFile.cs` as the .NET Standard 2.0 artifact, and document the APIs in the string section of `docs/assertion-overview.md`. Benchmark the read-only span core because all public receiver shapes delegate to it, while also including representative string predicates and successful guards to detect accidental wrapper allocations. diff --git a/benchmarks/Light.GuardClauses.Performance/StringAssertions/StringInspectionBenchmark.cs b/benchmarks/Light.GuardClauses.Performance/StringAssertions/StringInspectionBenchmark.cs new file mode 100644 index 00000000..502a5d28 --- /dev/null +++ b/benchmarks/Light.GuardClauses.Performance/StringAssertions/StringInspectionBenchmark.cs @@ -0,0 +1,143 @@ +using System; +using System.Buffers.Text; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; + +namespace Light.GuardClauses.Performance.StringAssertions; + +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +[CategoriesColumn] +public class StringInspectionBenchmark +{ + public enum InspectionScenario + { + ShortSuccess, + LongSuccess, + EarlyFailure, + MiddleFailure, + LateFailure, + MixedUnicodeSuccess, + } + + private string _alphanumeric = null!; + private string _digits = null!; + + public string SuccessfulGuardInput = new ('7', 256); + + [Params( + InspectionScenario.ShortSuccess, + InspectionScenario.LongSuccess, + InspectionScenario.EarlyFailure, + InspectionScenario.MiddleFailure, + InspectionScenario.LateFailure, + InspectionScenario.MixedUnicodeSuccess + )] + public InspectionScenario Scenario { get; set; } + + [GlobalSetup] + public void Setup() => + (_digits, _alphanumeric) = Scenario switch + { + InspectionScenario.ShortSuccess => ("12345678", "Abcd1234"), + InspectionScenario.LongSuccess => (new ('7', 4_096), new ('A', 4_096)), + InspectionScenario.EarlyFailure => ("x" + new string('7', 4_095), "!" + new string('A', 4_095)), + InspectionScenario.MiddleFailure => + (new string('7', 2_048) + "x" + new string('7', 2_047), + new string('A', 2_048) + "!" + new string('A', 2_047)), + InspectionScenario.LateFailure => (new string('7', 4_095) + "x", new string('A', 4_095) + "!"), + InspectionScenario.MixedUnicodeSuccess => (new ('٢', 4_096), new ('Ω', 4_096)), + _ => throw new ArgumentOutOfRangeException(), + }; + + [Benchmark(Baseline = true)] + [BenchmarkCategory("Digits")] + public bool ImperativeDigits() + { + foreach (var character in _digits.AsSpan()) + { + if (!char.IsDigit(character)) + { + return false; + } + } + + return true; + } + + [Benchmark] + [BenchmarkCategory("Digits")] + public bool ContainsOnlyDigits() => _digits.AsSpan().ContainsOnlyDigits(); + + [Benchmark(Baseline = true)] + [BenchmarkCategory("LettersOrDigits")] + public bool ImperativeLettersOrDigits() + { + foreach (var character in _alphanumeric.AsSpan()) + { + if (!char.IsLetterOrDigit(character)) + { + return false; + } + } + + return true; + } + + [Benchmark] + [BenchmarkCategory("LettersOrDigits")] + public bool ContainsOnlyLettersOrDigits() => _alphanumeric.AsSpan().ContainsOnlyLettersOrDigits(); + + [Benchmark(Baseline = true)] + [BenchmarkCategory("StringPredicate")] + public bool ImperativeStringDigits() => ImperativeDigits(); + + [Benchmark] + [BenchmarkCategory("StringPredicate")] + public bool StringContainsOnlyDigits() => _digits.ContainsOnlyDigits(); + + [Benchmark(Baseline = true)] + [BenchmarkCategory("SuccessfulGuard")] + public string ImperativeSuccessfulGuard() + { + foreach (var character in SuccessfulGuardInput.AsSpan()) + { + if (!char.IsDigit(character)) + { + throw new InvalidOperationException(); + } + } + + return SuccessfulGuardInput; + } + + [Benchmark] + [BenchmarkCategory("SuccessfulGuard")] + public string MustContainOnlyDigits() => SuccessfulGuardInput.MustContainOnlyDigits(); + + [Benchmark(Baseline = true)] + [BenchmarkCategory("Base64")] + public bool FrameworkBase64() => Base64.IsValid("VGhlIHF1aWNrIGJyb3duIGZveA==".AsSpan()); + + [Benchmark] + [BenchmarkCategory("Base64")] + public bool IsBase64() => "VGhlIHF1aWNrIGJyb3duIGZveA==".IsBase64(); + + [Benchmark(Baseline = true)] + [BenchmarkCategory("NoWhiteSpaceGuard")] + public string ImperativeNoWhiteSpaceGuard() + { + foreach (var character in SuccessfulGuardInput.AsSpan()) + { + if (char.IsWhiteSpace(character)) + { + throw new InvalidOperationException(); + } + } + + return SuccessfulGuardInput; + } + + [Benchmark] + [BenchmarkCategory("NoWhiteSpaceGuard")] + public string MustNotContainWhiteSpace() => SuccessfulGuardInput.MustNotContainWhiteSpace(); +} diff --git a/docs/assertion-overview.md b/docs/assertion-overview.md index 3bbf9f01..05e0feac 100644 --- a/docs/assertion-overview.md +++ b/docs/assertion-overview.md @@ -110,6 +110,13 @@ The dictionary key guards bind to any dictionary type implementing `IReadOnlyDic | `IsNullOrWhiteSpace`, `MustNotBeNullOrWhiteSpace` | Test or reject null, empty, or all-whitespace strings | | `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 | +| `ContainsOnlyDigits`, `MustContainOnlyDigits` | Test or require Unicode decimal-digit content | +| `ContainsOnlyLettersOrDigits`, `MustContainOnlyLettersOrDigits` | Test or require Unicode letter-or-decimal-digit content | +| `IsUpperCase`, `MustBeUpperCase` | Test or require the absence of Unicode lowercase characters | +| `IsLowerCase`, `MustBeLowerCase` | Test or require the absence of Unicode uppercase characters | +| `MustNotContainWhiteSpace` | Reject any Unicode whitespace character | +| `IsBase64`, `MustBeBase64` | Test or require structurally valid standard Base64 without decoding | +| `IsHexadecimal`, `MustBeHexadecimal` | Test or require ASCII hexadecimal characters (`0`-`9`, `A`-`F`, and `a`-`f`) | | `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 | @@ -129,6 +136,10 @@ The dictionary key guards bind to any dictionary type implementing `IReadOnlyDic The `Light.GuardClauses.FrameworkExtensions.StringExtensions.Contains` companion method supplies `string.Contains(string, StringComparison)` without colliding with the `Check` assertion namespace. +The string-inspection predicates and guards support `string`, `Span`, `ReadOnlySpan`, `Memory`, and `ReadOnlyMemory`. A null string fails predicates and causes default guards to throw `ArgumentNullException`; empty inputs satisfy every inspection. Digit, letter-or-digit, casing, and whitespace checks use .NET's invariant Unicode character classification. “Upper case” means no lowercase character is present, while “lower case” means no uppercase character is present, so uncased letters, digits, punctuation, symbols, whitespace, and empty inputs can satisfy both. Hexadecimal deliberately accepts only ASCII characters and imposes no length, prefix, or numeric-range rule. + +Base64 inspection validates the standard `A`-`Z`, `a`-`z`, `0`-`9`, `+`, and `/` alphabet and legal quartet/padding structure. It ignores only space, tab, carriage return, and line feed anywhere in the input; Base64Url-only characters are invalid. No inspection allocates, copies, normalizes, changes case, decodes, invokes a regular expression, or uses LINQ. All scans stop as soon as failure is known and otherwise use O(n) time with constant additional space. .NET 10 delegates Base64 validation to the framework's optimized `Base64.IsValid`; portable targets use the equivalent allocation-free scalar validator. Other Unicode-sensitive scans stay single-pass scalar implementations on every target so their observable classification remains identical. + ## Date and time assertions | Assertion | Behavior | diff --git a/docs/contributing-and-building.md b/docs/contributing-and-building.md index ef718c74..7ac857be 100644 --- a/docs/contributing-and-building.md +++ b/docs/contributing-and-building.md @@ -13,9 +13,12 @@ From the repository root, use the single solution entry point: ```shell dotnet restore Light.GuardClauses.slnx dotnet build Light.GuardClauses.slnx -c Release --no-restore -dotnet test Light.GuardClauses.slnx -c Release --no-build +dotnet test --solution Light.GuardClauses.slnx -c Release --no-build --no-restore ``` +Tests run on Microsoft Testing Platform. To collect Cobertura coverage locally, add +`--results-directory artifacts/test-results --coverage --coverage-output-format cobertura` to the test command. + 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/global.json b/global.json index 50baaf1a..bcdddea0 100644 --- a/global.json +++ b/global.json @@ -2,5 +2,8 @@ "sdk": { "version": "10.0.300", "rollForward": "latestPatch" + }, + "test": { + "runner": "Microsoft.Testing.Platform" } } diff --git a/src/Light.GuardClauses/Check.ContainsOnlyDigits.cs b/src/Light.GuardClauses/Check.ContainsOnlyDigits.cs new file mode 100644 index 00000000..3bfde9e8 --- /dev/null +++ b/src/Light.GuardClauses/Check.ContainsOnlyDigits.cs @@ -0,0 +1,69 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// + /// Checks if the specified string is non-null and every character is a Unicode decimal digit. Empty strings are valid. + /// + /// The string to be checked. + /// + /// True if is non-null and every character is a Unicode decimal digit, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ContainsOnlyDigits(this string? parameter) => + parameter is not null && parameter.AsSpan().ContainsOnlyDigits(); + + /// + /// Checks if every character in the specified span is a Unicode decimal digit. Empty spans are valid. + /// + /// The character span to be checked. + /// + /// True if every character in is a Unicode decimal digit, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ContainsOnlyDigits(this Span parameter) => + ((ReadOnlySpan) parameter).ContainsOnlyDigits(); + + /// + /// Checks if every character in the specified read-only span is a Unicode decimal digit. Empty spans are valid. + /// + /// The read-only character span to be checked. + /// + /// True if every character in is a Unicode decimal digit, otherwise false. + /// + public static bool ContainsOnlyDigits(this ReadOnlySpan parameter) + { + foreach (var character in parameter) + { + if (!char.IsDigit(character)) + { + return false; + } + } + + return true; + } + + /// + /// Checks if every character in the specified memory is a Unicode decimal digit. Empty memory is valid. + /// + /// The character memory to be checked. + /// + /// True if every character in is a Unicode decimal digit, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ContainsOnlyDigits(this Memory parameter) => parameter.Span.ContainsOnlyDigits(); + + /// + /// Checks if every character in the specified read-only memory is a Unicode decimal digit. Empty memory is valid. + /// + /// The read-only character memory to be checked. + /// + /// True if every character in is a Unicode decimal digit, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ContainsOnlyDigits(this ReadOnlyMemory parameter) => parameter.Span.ContainsOnlyDigits(); +} diff --git a/src/Light.GuardClauses/Check.ContainsOnlyLettersOrDigits.cs b/src/Light.GuardClauses/Check.ContainsOnlyLettersOrDigits.cs new file mode 100644 index 00000000..af4e20e8 --- /dev/null +++ b/src/Light.GuardClauses/Check.ContainsOnlyLettersOrDigits.cs @@ -0,0 +1,71 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// + /// Checks if the specified string is non-null and every character is a Unicode letter or decimal digit. Empty strings are valid. + /// + /// The string to be checked. + /// + /// True if is non-null and every character is a Unicode letter or decimal digit, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ContainsOnlyLettersOrDigits(this string? parameter) => + parameter is not null && parameter.AsSpan().ContainsOnlyLettersOrDigits(); + + /// + /// Checks if every character in the specified span is a Unicode letter or decimal digit. Empty spans are valid. + /// + /// The character span to be checked. + /// + /// True if every character in is a Unicode letter or decimal digit, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ContainsOnlyLettersOrDigits(this Span parameter) => + ((ReadOnlySpan) parameter).ContainsOnlyLettersOrDigits(); + + /// + /// Checks if every character in the specified read-only span is a Unicode letter or decimal digit. Empty spans are valid. + /// + /// The read-only character span to be checked. + /// + /// True if every character in is a Unicode letter or decimal digit, otherwise false. + /// + public static bool ContainsOnlyLettersOrDigits(this ReadOnlySpan parameter) + { + foreach (var character in parameter) + { + if (!char.IsLetterOrDigit(character)) + { + return false; + } + } + + return true; + } + + /// + /// Checks if every character in the specified memory is a Unicode letter or decimal digit. Empty memory is valid. + /// + /// The character memory to be checked. + /// + /// True if every character in is a Unicode letter or decimal digit, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ContainsOnlyLettersOrDigits(this Memory parameter) => + parameter.Span.ContainsOnlyLettersOrDigits(); + + /// + /// Checks if every character in the specified read-only memory is a Unicode letter or decimal digit. Empty memory is valid. + /// + /// The read-only character memory to be checked. + /// + /// True if every character in is a Unicode letter or decimal digit, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ContainsOnlyLettersOrDigits(this ReadOnlyMemory parameter) => + parameter.Span.ContainsOnlyLettersOrDigits(); +} diff --git a/src/Light.GuardClauses/Check.IsBase64.cs b/src/Light.GuardClauses/Check.IsBase64.cs new file mode 100644 index 00000000..03b599d7 --- /dev/null +++ b/src/Light.GuardClauses/Check.IsBase64.cs @@ -0,0 +1,143 @@ +using System; +using System.Runtime.CompilerServices; +#if NET10_0_OR_GREATER +using System.Buffers.Text; +#endif + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// + /// Checks if the string is non-null and valid standard Base64. Space, tab, carriage return, and line feed are ignored. + /// Empty and whitespace-only strings are valid. + /// + /// The string to be checked. + /// True if is non-null and contains valid standard Base64, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsBase64(this string? parameter) => parameter is not null && parameter.AsSpan().IsBase64(); + + /// + /// Checks if the span is valid standard Base64. Space, tab, carriage return, and line feed are ignored. + /// Empty and whitespace-only spans are valid. + /// + /// The character span to be checked. + /// True if contains valid standard Base64, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsBase64(this Span parameter) => ((ReadOnlySpan) parameter).IsBase64(); + + /// + /// Checks if the read-only span is valid standard Base64. Space, tab, carriage return, and line feed are ignored. + /// Empty and whitespace-only spans are valid. + /// + /// The read-only character span to be checked. + /// True if contains valid standard Base64, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsBase64(this ReadOnlySpan parameter) + { +#if NET10_0_OR_GREATER + return Base64.IsValid(parameter); +#else + return IsBase64Portable(parameter); +#endif + } + + /// + /// Checks if the memory is valid standard Base64. Space, tab, carriage return, and line feed are ignored. + /// Empty and whitespace-only memory is valid. + /// + /// The character memory to be checked. + /// True if contains valid standard Base64, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsBase64(this Memory parameter) => parameter.Span.IsBase64(); + + /// + /// Checks if the read-only memory is valid standard Base64. Space, tab, carriage return, and line feed are ignored. + /// Empty and whitespace-only memory is valid. + /// + /// The read-only character memory to be checked. + /// True if contains valid standard Base64, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsBase64(this ReadOnlyMemory parameter) => parameter.Span.IsBase64(); + + private static bool IsBase64Portable(ReadOnlySpan parameter) + { + var nonWhiteSpaceCount = 0; + var paddingCount = 0; + var lastDataValue = 0; + + foreach (var character in parameter) + { + if (character is ' ' or '\t' or '\r' or '\n') + { + continue; + } + + ++nonWhiteSpaceCount; + + if (character == '=') + { + if (++paddingCount > 2) + { + return false; + } + + continue; + } + + if (paddingCount != 0 || !TryGetBase64Value(character, out lastDataValue)) + { + return false; + } + } + + if ((nonWhiteSpaceCount & 3) != 0) + { + return false; + } + + return paddingCount switch + { + 0 => true, + 1 => nonWhiteSpaceCount >= 4 && (lastDataValue & 0b11) == 0, + 2 => nonWhiteSpaceCount >= 4 && (lastDataValue & 0b1111) == 0, + _ => false, + }; + } + + private static bool TryGetBase64Value(char character, out int value) + { + if (character is >= 'A' and <= 'Z') + { + value = character - 'A'; + return true; + } + + if (character is >= 'a' and <= 'z') + { + value = character - 'a' + 26; + return true; + } + + if (character is >= '0' and <= '9') + { + value = character - '0' + 52; + return true; + } + + if (character == '+') + { + value = 62; + return true; + } + + if (character == '/') + { + value = 63; + return true; + } + + value = 0; + return false; + } +} diff --git a/src/Light.GuardClauses/Check.IsHexadecimal.cs b/src/Light.GuardClauses/Check.IsHexadecimal.cs new file mode 100644 index 00000000..83ab2436 --- /dev/null +++ b/src/Light.GuardClauses/Check.IsHexadecimal.cs @@ -0,0 +1,78 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// + /// Checks if the specified string is non-null and contains only ASCII hexadecimal characters. Empty strings are valid. + /// + /// The string to be checked. + /// + /// True if is non-null and contains only ASCII hexadecimal characters, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsHexadecimal(this string? parameter) => + parameter is not null && parameter.AsSpan().IsHexadecimal(); + + /// + /// Checks if the specified span contains only ASCII hexadecimal characters. Empty spans are valid. + /// + /// The character span to be checked. + /// + /// True if contains only ASCII hexadecimal characters, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsHexadecimal(this Span parameter) => ((ReadOnlySpan) parameter).IsHexadecimal(); + + /// + /// Checks if the specified read-only span contains only ASCII hexadecimal characters. Empty spans are valid. + /// + /// The read-only character span to be checked. + /// + /// True if contains only ASCII hexadecimal characters, otherwise false. + /// + public static bool IsHexadecimal(this ReadOnlySpan parameter) + { + foreach (var character in parameter) + { +#if NET10_0_OR_GREATER + if (!char.IsAsciiHexDigit(character)) +#else + if (!IsAsciiHexDigit(character)) +#endif + { + return false; + } + } + + return true; + } + +#if !NET10_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsAsciiHexDigit(char character) => + character is >= '0' and <= '9' or >= 'A' and <= 'F' or >= 'a' and <= 'f'; +#endif + + /// + /// Checks if the specified memory contains only ASCII hexadecimal characters. Empty memory is valid. + /// + /// The character memory to be checked. + /// + /// True if contains only ASCII hexadecimal characters, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsHexadecimal(this Memory parameter) => parameter.Span.IsHexadecimal(); + + /// + /// Checks if the specified read-only memory contains only ASCII hexadecimal characters. Empty memory is valid. + /// + /// The read-only character memory to be checked. + /// + /// True if contains only ASCII hexadecimal characters, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsHexadecimal(this ReadOnlyMemory parameter) => parameter.Span.IsHexadecimal(); +} diff --git a/src/Light.GuardClauses/Check.IsLowerCase.cs b/src/Light.GuardClauses/Check.IsLowerCase.cs new file mode 100644 index 00000000..e3c91c0b --- /dev/null +++ b/src/Light.GuardClauses/Check.IsLowerCase.cs @@ -0,0 +1,59 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// + /// Checks if the specified string is non-null and contains no Unicode uppercase character. Empty strings are valid. + /// + /// The string to be checked. + /// + /// True if is non-null and contains no Unicode uppercase character, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsLowerCase(this string? parameter) => parameter is not null && parameter.AsSpan().IsLowerCase(); + + /// + /// Checks if the specified span contains no Unicode uppercase character. Empty spans are valid. + /// + /// The character span to be checked. + /// True if contains no Unicode uppercase character, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsLowerCase(this Span parameter) => ((ReadOnlySpan) parameter).IsLowerCase(); + + /// + /// Checks if the specified read-only span contains no Unicode uppercase character. Empty spans are valid. + /// + /// The read-only character span to be checked. + /// True if contains no Unicode uppercase character, otherwise false. + public static bool IsLowerCase(this ReadOnlySpan parameter) + { + foreach (var character in parameter) + { + if (char.IsUpper(character)) + { + return false; + } + } + + return true; + } + + /// + /// Checks if the specified memory contains no Unicode uppercase character. Empty memory is valid. + /// + /// The character memory to be checked. + /// True if contains no Unicode uppercase character, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsLowerCase(this Memory parameter) => parameter.Span.IsLowerCase(); + + /// + /// Checks if the specified read-only memory contains no Unicode uppercase character. Empty memory is valid. + /// + /// The read-only character memory to be checked. + /// True if contains no Unicode uppercase character, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsLowerCase(this ReadOnlyMemory parameter) => parameter.Span.IsLowerCase(); +} diff --git a/src/Light.GuardClauses/Check.IsUpperCase.cs b/src/Light.GuardClauses/Check.IsUpperCase.cs new file mode 100644 index 00000000..63b8ed41 --- /dev/null +++ b/src/Light.GuardClauses/Check.IsUpperCase.cs @@ -0,0 +1,59 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// + /// Checks if the specified string is non-null and contains no Unicode lowercase character. Empty strings are valid. + /// + /// The string to be checked. + /// + /// True if is non-null and contains no Unicode lowercase character, otherwise false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsUpperCase(this string? parameter) => parameter is not null && parameter.AsSpan().IsUpperCase(); + + /// + /// Checks if the specified span contains no Unicode lowercase character. Empty spans are valid. + /// + /// The character span to be checked. + /// True if contains no Unicode lowercase character, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsUpperCase(this Span parameter) => ((ReadOnlySpan) parameter).IsUpperCase(); + + /// + /// Checks if the specified read-only span contains no Unicode lowercase character. Empty spans are valid. + /// + /// The read-only character span to be checked. + /// True if contains no Unicode lowercase character, otherwise false. + public static bool IsUpperCase(this ReadOnlySpan parameter) + { + foreach (var character in parameter) + { + if (char.IsLower(character)) + { + return false; + } + } + + return true; + } + + /// + /// Checks if the specified memory contains no Unicode lowercase character. Empty memory is valid. + /// + /// The character memory to be checked. + /// True if contains no Unicode lowercase character, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsUpperCase(this Memory parameter) => parameter.Span.IsUpperCase(); + + /// + /// Checks if the specified read-only memory contains no Unicode lowercase character. Empty memory is valid. + /// + /// The read-only character memory to be checked. + /// True if contains no Unicode lowercase character, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsUpperCase(this ReadOnlyMemory parameter) => parameter.Span.IsUpperCase(); +} diff --git a/src/Light.GuardClauses/Check.MustBeAscii.cs b/src/Light.GuardClauses/Check.MustBeAscii.cs index 1951ec9b..c43bd7bb 100644 --- a/src/Light.GuardClauses/Check.MustBeAscii.cs +++ b/src/Light.GuardClauses/Check.MustBeAscii.cs @@ -2,13 +2,20 @@ using System.Runtime.CompilerServices; using JetBrains.Annotations; using Light.GuardClauses.ExceptionFactory; +using Light.GuardClauses.Exceptions; using NotNullAttribute = System.Diagnostics.CodeAnalysis.NotNullAttribute; namespace Light.GuardClauses; public static partial class Check { - /// Ensures that the character is ASCII, or otherwise throws an . + /// + /// Ensures that the character is ASCII, or otherwise throws an . + /// + /// The character to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static char MustBeAscii( this char parameter, @@ -24,7 +31,14 @@ public static char MustBeAscii( return parameter; } - /// Ensures that the character is ASCII, or otherwise throws your custom exception. + /// + /// Ensures that the character is ASCII, or otherwise throws your custom exception. + /// + /// The character to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character. [MethodImpl(MethodImplOptions.AggressiveInlining)] [ContractAnnotation("exceptionFactory:null => halt")] public static char MustBeAscii(this char parameter, Func exceptionFactory) @@ -37,7 +51,13 @@ public static char MustBeAscii(this char parameter, Func except return parameter; } - /// Ensures that the byte is ASCII, or otherwise throws an . + /// + /// Ensures that the byte is ASCII, or otherwise throws an . + /// + /// The byte to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated byte. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static byte MustBeAscii( this byte parameter, @@ -53,7 +73,14 @@ public static byte MustBeAscii( return parameter; } - /// Ensures that the byte is ASCII, or otherwise throws your custom exception. + /// + /// Ensures that the byte is ASCII, or otherwise throws your custom exception. + /// + /// The byte to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated byte. [MethodImpl(MethodImplOptions.AggressiveInlining)] [ContractAnnotation("exceptionFactory:null => halt")] public static byte MustBeAscii(this byte parameter, Func exceptionFactory) @@ -66,7 +93,13 @@ public static byte MustBeAscii(this byte parameter, Func except return parameter; } - /// Ensures that the string is non-null and contains only ASCII characters. + /// + /// Ensures that the string is non-null and contains only ASCII characters, or otherwise throws a . + /// + /// The string to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated string. [MethodImpl(MethodImplOptions.AggressiveInlining)] [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] public static string MustBeAscii( @@ -78,13 +111,20 @@ public static string MustBeAscii( parameter.MustNotBeNull(parameterName, message); if (!parameter.IsAscii()) { - Throw.Argument(parameterName, message ?? $"{parameterName ?? "The string"} must contain only ASCII characters."); + Throw.InvalidStringContent(parameterName, message, "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. + /// + /// Ensures that the string is non-null and contains only ASCII characters, or otherwise throws your custom exception. + /// + /// The string to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated string. [MethodImpl(MethodImplOptions.AggressiveInlining)] [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull; exceptionFactory:null => halt")] public static string MustBeAscii( @@ -100,7 +140,13 @@ public static string MustBeAscii( return parameter; } - /// Ensures that the character span contains only ASCII characters. + /// + /// Ensures that the character span contains only ASCII characters, or otherwise throws a . + /// + /// The character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character span. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Span MustBeAscii( this Span parameter, @@ -112,7 +158,14 @@ public static Span MustBeAscii( return parameter; } - /// Ensures that the character span contains only ASCII characters, or otherwise throws your custom exception. + /// + /// Ensures that the character span contains only ASCII characters, or otherwise throws your custom exception. + /// + /// The character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character span. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Span MustBeAscii( this Span parameter, @@ -123,7 +176,13 @@ ReadOnlySpanExceptionFactory exceptionFactory return parameter; } - /// Ensures that the read-only character span contains only ASCII characters. + /// + /// Ensures that the read-only character span contains only ASCII characters, or otherwise throws a . + /// + /// The read-only character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character span. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ReadOnlySpan MustBeAscii( this ReadOnlySpan parameter, @@ -133,13 +192,20 @@ public static ReadOnlySpan MustBeAscii( { if (!parameter.IsAscii()) { - Throw.Argument(parameterName, message ?? $"{parameterName ?? "The character span"} must contain only ASCII characters."); + Throw.InvalidStringContent(parameterName, message, "must contain only ASCII characters"); } return parameter; } - /// Ensures that the read-only character span contains only ASCII characters, or otherwise throws your custom exception. + /// + /// Ensures that the read-only character span contains only ASCII characters, or otherwise throws your custom exception. + /// + /// The read-only character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character span. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ReadOnlySpan MustBeAscii( this ReadOnlySpan parameter, @@ -154,7 +220,13 @@ ReadOnlySpanExceptionFactory exceptionFactory return parameter; } - /// Ensures that the character memory contains only ASCII characters. + /// + /// Ensures that the character memory contains only ASCII characters, or otherwise throws a . + /// + /// The character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character memory. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Memory MustBeAscii( this Memory parameter, @@ -166,7 +238,14 @@ public static Memory MustBeAscii( return parameter; } - /// Ensures that the character memory contains only ASCII characters, or otherwise throws your custom exception. + /// + /// Ensures that the character memory contains only ASCII characters, or otherwise throws your custom exception. + /// + /// The character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character memory. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Memory MustBeAscii( this Memory parameter, @@ -177,7 +256,13 @@ ReadOnlySpanExceptionFactory exceptionFactory return parameter; } - /// Ensures that the read-only character memory contains only ASCII characters. + /// + /// Ensures that the read-only character memory contains only ASCII characters, or otherwise throws a . + /// + /// The read-only character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character memory. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ReadOnlyMemory MustBeAscii( this ReadOnlyMemory parameter, @@ -189,7 +274,14 @@ public static ReadOnlyMemory MustBeAscii( return parameter; } - /// Ensures that the read-only character memory contains only ASCII characters, or otherwise throws your custom exception. + /// + /// Ensures that the read-only character memory contains only ASCII characters, or otherwise throws your custom exception. + /// + /// The read-only character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character memory. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ReadOnlyMemory MustBeAscii( this ReadOnlyMemory parameter, @@ -200,7 +292,13 @@ ReadOnlySpanExceptionFactory exceptionFactory return parameter; } - /// Ensures that the byte span contains only ASCII values. + /// + /// Ensures that the byte span contains only ASCII values. + /// + /// The byte span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated byte span. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Span MustBeAscii( this Span parameter, @@ -212,7 +310,14 @@ public static Span MustBeAscii( return parameter; } - /// Ensures that the byte span contains only ASCII values, or otherwise throws your custom exception. + /// + /// Ensures that the byte span contains only ASCII values, or otherwise throws your custom exception. + /// + /// The byte span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated byte span. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Span MustBeAscii( this Span parameter, @@ -223,7 +328,13 @@ ReadOnlySpanExceptionFactory exceptionFactory return parameter; } - /// Ensures that the read-only byte span contains only ASCII values. + /// + /// Ensures that the read-only byte span contains only ASCII values. + /// + /// The read-only byte span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only byte span. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ReadOnlySpan MustBeAscii( this ReadOnlySpan parameter, @@ -239,7 +350,14 @@ public static ReadOnlySpan MustBeAscii( return parameter; } - /// Ensures that the read-only byte span contains only ASCII values, or otherwise throws your custom exception. + /// + /// Ensures that the read-only byte span contains only ASCII values, or otherwise throws your custom exception. + /// + /// The read-only byte span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only byte span. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ReadOnlySpan MustBeAscii( this ReadOnlySpan parameter, @@ -254,7 +372,13 @@ ReadOnlySpanExceptionFactory exceptionFactory return parameter; } - /// Ensures that the byte memory contains only ASCII values. + /// + /// Ensures that the byte memory contains only ASCII values. + /// + /// The byte memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated byte memory. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Memory MustBeAscii( this Memory parameter, @@ -266,7 +390,14 @@ public static Memory MustBeAscii( return parameter; } - /// Ensures that the byte memory contains only ASCII values, or otherwise throws your custom exception. + /// + /// Ensures that the byte memory contains only ASCII values, or otherwise throws your custom exception. + /// + /// The byte memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated byte memory. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Memory MustBeAscii( this Memory parameter, @@ -277,7 +408,13 @@ ReadOnlySpanExceptionFactory exceptionFactory return parameter; } - /// Ensures that the read-only byte memory contains only ASCII values. + /// + /// Ensures that the read-only byte memory contains only ASCII values. + /// + /// The read-only byte memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only byte memory. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ReadOnlyMemory MustBeAscii( this ReadOnlyMemory parameter, @@ -289,7 +426,14 @@ public static ReadOnlyMemory MustBeAscii( return parameter; } - /// Ensures that the read-only byte memory contains only ASCII values, or otherwise throws your custom exception. + /// + /// Ensures that the read-only byte memory contains only ASCII values, or otherwise throws your custom exception. + /// + /// The read-only byte memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only byte memory. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ReadOnlyMemory MustBeAscii( this ReadOnlyMemory parameter, diff --git a/src/Light.GuardClauses/Check.MustBeBase64.cs b/src/Light.GuardClauses/Check.MustBeBase64.cs new file mode 100644 index 00000000..a65a7473 --- /dev/null +++ b/src/Light.GuardClauses/Check.MustBeBase64.cs @@ -0,0 +1,205 @@ +using System; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using Light.GuardClauses.ExceptionFactory; +using Light.GuardClauses.Exceptions; +using NotNullAttribute = System.Diagnostics.CodeAnalysis.NotNullAttribute; + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// + /// Ensures that the string is standard Base64 with valid padding. Space, tab, carriage return, and line feed are ignored. + /// Empty and whitespace-only strings are valid. + /// + /// The string to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated string. + /// Thrown when is null. + /// Thrown when the string is not valid Base64. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static string MustBeBase64( + [NotNull][ValidatedNotNull] this string? parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + parameter.MustNotBeNull(parameterName, message); + if (!parameter.IsBase64()) + { + Throw.InvalidStringContent(parameterName, message, "must be valid standard Base64"); + } + + return parameter; + } + + /// + /// Ensures that the string is valid standard Base64, or otherwise throws your custom exception. + /// + /// The string to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated string. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull; exceptionFactory:null => halt")] + public static string MustBeBase64( + [NotNull][ValidatedNotNull] this string? parameter, + Func exceptionFactory + ) + { + if (parameter is null || !parameter.IsBase64()) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the span is valid standard Base64. Empty and supported-whitespace-only spans are valid. + /// + /// The character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeBase64( + this Span parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter).MustBeBase64(parameterName, message); + return parameter; + } + + /// + /// Ensures that the span is valid standard Base64, or otherwise throws your custom exception. + /// + /// The character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeBase64(this Span parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + ((ReadOnlySpan) parameter).MustBeBase64(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only span is valid standard Base64. Empty and supported-whitespace-only spans are valid. + /// + /// The read-only character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character span. + public static ReadOnlySpan MustBeBase64( + this ReadOnlySpan parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + if (!parameter.IsBase64()) + { + Throw.InvalidStringContent(parameterName, message, "must be valid standard Base64"); + } + + return parameter; + } + + /// + /// Ensures that the read-only span is valid standard Base64, or otherwise throws your custom exception. + /// + /// The read-only character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character span. + public static ReadOnlySpan MustBeBase64( + this ReadOnlySpan parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + if (!parameter.IsBase64()) + { + Throw.CustomSpanException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the memory is valid standard Base64. Empty and supported-whitespace-only memory is valid. + /// + /// The character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeBase64( + this Memory parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter.Span).MustBeBase64(parameterName, message); + return parameter; + } + + /// + /// Ensures that the memory is valid standard Base64, or otherwise throws your custom exception. + /// + /// The character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeBase64(this Memory parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + ((ReadOnlySpan) parameter.Span).MustBeBase64(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only memory is valid standard Base64. Empty and supported-whitespace-only memory is valid. + /// + /// The read-only character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeBase64( + this ReadOnlyMemory parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + parameter.Span.MustBeBase64(parameterName, message); + return parameter; + } + + /// + /// Ensures that the read-only memory is valid standard Base64, or otherwise throws your custom exception. + /// + /// The read-only character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeBase64( + this ReadOnlyMemory parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + parameter.Span.MustBeBase64(exceptionFactory); + return parameter; + } +} diff --git a/src/Light.GuardClauses/Check.MustBeHexadecimal.cs b/src/Light.GuardClauses/Check.MustBeHexadecimal.cs new file mode 100644 index 00000000..e9cdcbfc --- /dev/null +++ b/src/Light.GuardClauses/Check.MustBeHexadecimal.cs @@ -0,0 +1,204 @@ +using System; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using Light.GuardClauses.ExceptionFactory; +using Light.GuardClauses.Exceptions; +using NotNullAttribute = System.Diagnostics.CodeAnalysis.NotNullAttribute; + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// + /// Ensures that the string contains only ASCII hexadecimal characters. Empty strings are valid. + /// + /// The string to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated string. + /// Thrown when is null. + /// Thrown when the string contains a non-hexadecimal character. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static string MustBeHexadecimal( + [NotNull][ValidatedNotNull] this string? parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + parameter.MustNotBeNull(parameterName, message); + if (!parameter.IsHexadecimal()) + { + Throw.InvalidStringContent(parameterName, message, "must contain only ASCII hexadecimal characters"); + } + + return parameter; + } + + /// + /// Ensures that the string contains only ASCII hexadecimal characters, or otherwise throws your custom exception. + /// + /// The string to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated string. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull; exceptionFactory:null => halt")] + public static string MustBeHexadecimal( + [NotNull][ValidatedNotNull] this string? parameter, + Func exceptionFactory + ) + { + if (parameter is null || !parameter.IsHexadecimal()) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the span contains only ASCII hexadecimal characters. Empty spans are valid. + /// + /// The character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeHexadecimal( + this Span parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter).MustBeHexadecimal(parameterName, message); + return parameter; + } + + /// + /// Ensures that the span contains only ASCII hexadecimal characters, or otherwise throws your custom exception. + /// + /// The character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeHexadecimal(this Span parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + ((ReadOnlySpan) parameter).MustBeHexadecimal(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only span contains only ASCII hexadecimal characters. Empty spans are valid. + /// + /// The read-only character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character span. + public static ReadOnlySpan MustBeHexadecimal( + this ReadOnlySpan parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + if (!parameter.IsHexadecimal()) + { + Throw.InvalidStringContent(parameterName, message, "must contain only ASCII hexadecimal characters"); + } + + return parameter; + } + + /// + /// Ensures that the read-only span contains only ASCII hexadecimal characters, or otherwise throws your custom exception. + /// + /// The read-only character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character span. + public static ReadOnlySpan MustBeHexadecimal( + this ReadOnlySpan parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + if (!parameter.IsHexadecimal()) + { + Throw.CustomSpanException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the memory contains only ASCII hexadecimal characters. Empty memory is valid. + /// + /// The character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeHexadecimal( + this Memory parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter.Span).MustBeHexadecimal(parameterName, message); + return parameter; + } + + /// + /// Ensures that the memory contains only ASCII hexadecimal characters, or otherwise throws your custom exception. + /// + /// The character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeHexadecimal(this Memory parameter, ReadOnlySpanExceptionFactory exceptionFactory) + { + ((ReadOnlySpan) parameter.Span).MustBeHexadecimal(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only memory contains only ASCII hexadecimal characters. Empty memory is valid. + /// + /// The read-only character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeHexadecimal( + this ReadOnlyMemory parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + parameter.Span.MustBeHexadecimal(parameterName, message); + return parameter; + } + + /// + /// Ensures that the read-only memory contains only ASCII hexadecimal characters, or otherwise throws your custom exception. + /// + /// The read-only character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeHexadecimal( + this ReadOnlyMemory parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + parameter.Span.MustBeHexadecimal(exceptionFactory); + return parameter; + } +} diff --git a/src/Light.GuardClauses/Check.MustBeLowerCase.cs b/src/Light.GuardClauses/Check.MustBeLowerCase.cs new file mode 100644 index 00000000..3cc02728 --- /dev/null +++ b/src/Light.GuardClauses/Check.MustBeLowerCase.cs @@ -0,0 +1,210 @@ +using System; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using Light.GuardClauses.ExceptionFactory; +using Light.GuardClauses.Exceptions; +using NotNullAttribute = System.Diagnostics.CodeAnalysis.NotNullAttribute; + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// + /// Ensures that the string contains no Unicode uppercase character. Empty and uncased strings are valid. + /// + /// The string to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated string. + /// Thrown when is null. + /// Thrown when the string contains an uppercase character. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static string MustBeLowerCase( + [NotNull] [ValidatedNotNull] this string? parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + parameter.MustNotBeNull(parameterName, message); + if (!parameter.IsLowerCase()) + { + Throw.InvalidStringContent(parameterName, message, "must contain no Unicode uppercase characters"); + } + + return parameter; + } + + /// + /// Ensures that the string contains no Unicode uppercase character, or otherwise throws your custom exception. + /// + /// The string to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated string. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull; exceptionFactory:null => halt")] + public static string MustBeLowerCase( + [NotNull] [ValidatedNotNull] this string? parameter, + Func exceptionFactory + ) + { + if (parameter is null || !parameter.IsLowerCase()) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the span contains no Unicode uppercase character. Empty and uncased spans are valid. + /// + /// The character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeLowerCase( + this Span parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter).MustBeLowerCase(parameterName, message); + return parameter; + } + + /// + /// Ensures that the span contains no Unicode uppercase character, or otherwise throws your custom exception. + /// + /// The character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeLowerCase( + this Span parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + ((ReadOnlySpan) parameter).MustBeLowerCase(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only span contains no Unicode uppercase character. Empty and uncased spans are valid. + /// + /// The read-only character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character span. + public static ReadOnlySpan MustBeLowerCase( + this ReadOnlySpan parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + if (!parameter.IsLowerCase()) + { + Throw.InvalidStringContent(parameterName, message, "must contain no Unicode uppercase characters"); + } + + return parameter; + } + + /// + /// Ensures that the read-only span contains no Unicode uppercase character, or otherwise throws your custom exception. + /// + /// The read-only character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character span. + public static ReadOnlySpan MustBeLowerCase( + this ReadOnlySpan parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + if (!parameter.IsLowerCase()) + { + Throw.CustomSpanException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the memory contains no Unicode uppercase character. Empty and uncased memory is valid. + /// + /// The character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeLowerCase( + this Memory parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter.Span).MustBeLowerCase(parameterName, message); + return parameter; + } + + /// + /// Ensures that the memory contains no Unicode uppercase character, or otherwise throws your custom exception. + /// + /// The character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeLowerCase( + this Memory parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + ((ReadOnlySpan) parameter.Span).MustBeLowerCase(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only memory contains no Unicode uppercase character. Empty and uncased memory is valid. + /// + /// The read-only character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeLowerCase( + this ReadOnlyMemory parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + parameter.Span.MustBeLowerCase(parameterName, message); + return parameter; + } + + /// + /// Ensures that the read-only memory contains no Unicode uppercase character, or otherwise throws your custom exception. + /// + /// The read-only character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeLowerCase( + this ReadOnlyMemory parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + parameter.Span.MustBeLowerCase(exceptionFactory); + return parameter; + } +} diff --git a/src/Light.GuardClauses/Check.MustBeUpperCase.cs b/src/Light.GuardClauses/Check.MustBeUpperCase.cs new file mode 100644 index 00000000..377e4568 --- /dev/null +++ b/src/Light.GuardClauses/Check.MustBeUpperCase.cs @@ -0,0 +1,210 @@ +using System; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using Light.GuardClauses.ExceptionFactory; +using Light.GuardClauses.Exceptions; +using NotNullAttribute = System.Diagnostics.CodeAnalysis.NotNullAttribute; + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// + /// Ensures that the string contains no Unicode lowercase character. Empty and uncased strings are valid. + /// + /// The string to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated string. + /// Thrown when is null. + /// Thrown when the string contains a lowercase character. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static string MustBeUpperCase( + [NotNull] [ValidatedNotNull] this string? parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + parameter.MustNotBeNull(parameterName, message); + if (!parameter.IsUpperCase()) + { + Throw.InvalidStringContent(parameterName, message, "must contain no Unicode lowercase characters"); + } + + return parameter; + } + + /// + /// Ensures that the string contains no Unicode lowercase character, or otherwise throws your custom exception. + /// + /// The string to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated string. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull; exceptionFactory:null => halt")] + public static string MustBeUpperCase( + [NotNull] [ValidatedNotNull] this string? parameter, + Func exceptionFactory + ) + { + if (parameter is null || !parameter.IsUpperCase()) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the span contains no Unicode lowercase character. Empty and uncased spans are valid. + /// + /// The character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeUpperCase( + this Span parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter).MustBeUpperCase(parameterName, message); + return parameter; + } + + /// + /// Ensures that the span contains no Unicode lowercase character, or otherwise throws your custom exception. + /// + /// The character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeUpperCase( + this Span parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + ((ReadOnlySpan) parameter).MustBeUpperCase(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only span contains no Unicode lowercase character. Empty and uncased spans are valid. + /// + /// The read-only character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character span. + public static ReadOnlySpan MustBeUpperCase( + this ReadOnlySpan parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + if (!parameter.IsUpperCase()) + { + Throw.InvalidStringContent(parameterName, message, "must contain no Unicode lowercase characters"); + } + + return parameter; + } + + /// + /// Ensures that the read-only span contains no Unicode lowercase character, or otherwise throws your custom exception. + /// + /// The read-only character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character span. + public static ReadOnlySpan MustBeUpperCase( + this ReadOnlySpan parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + if (!parameter.IsUpperCase()) + { + Throw.CustomSpanException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the memory contains no Unicode lowercase character. Empty and uncased memory is valid. + /// + /// The character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeUpperCase( + this Memory parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter.Span).MustBeUpperCase(parameterName, message); + return parameter; + } + + /// + /// Ensures that the memory contains no Unicode lowercase character, or otherwise throws your custom exception. + /// + /// The character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeUpperCase( + this Memory parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + ((ReadOnlySpan) parameter.Span).MustBeUpperCase(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only memory contains no Unicode lowercase character. Empty and uncased memory is valid. + /// + /// The read-only character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeUpperCase( + this ReadOnlyMemory parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + parameter.Span.MustBeUpperCase(parameterName, message); + return parameter; + } + + /// + /// Ensures that the read-only memory contains no Unicode lowercase character, or otherwise throws your custom exception. + /// + /// The read-only character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeUpperCase( + this ReadOnlyMemory parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + parameter.Span.MustBeUpperCase(exceptionFactory); + return parameter; + } +} diff --git a/src/Light.GuardClauses/Check.MustContainOnlyDigits.cs b/src/Light.GuardClauses/Check.MustContainOnlyDigits.cs new file mode 100644 index 00000000..752157bf --- /dev/null +++ b/src/Light.GuardClauses/Check.MustContainOnlyDigits.cs @@ -0,0 +1,210 @@ +using System; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using Light.GuardClauses.ExceptionFactory; +using Light.GuardClauses.Exceptions; +using NotNullAttribute = System.Diagnostics.CodeAnalysis.NotNullAttribute; + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// + /// Ensures that the string contains only Unicode decimal digits. Empty strings are valid. + /// + /// The string to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated string. + /// Thrown when is null. + /// Thrown when the string contains a non-digit character. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static string MustContainOnlyDigits( + [NotNull] [ValidatedNotNull] this string? parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + parameter.MustNotBeNull(parameterName, message); + if (!parameter.ContainsOnlyDigits()) + { + Throw.InvalidStringContent(parameterName, message, "must contain only Unicode decimal digits"); + } + + return parameter; + } + + /// + /// Ensures that the string contains only Unicode decimal digits, or otherwise throws your custom exception. + /// + /// The string to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated string. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull; exceptionFactory:null => halt")] + public static string MustContainOnlyDigits( + [NotNull] [ValidatedNotNull] this string? parameter, + Func exceptionFactory + ) + { + if (parameter is null || !parameter.ContainsOnlyDigits()) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the span contains only Unicode decimal digits. Empty spans are valid. + /// + /// The character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustContainOnlyDigits( + this Span parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter).MustContainOnlyDigits(parameterName, message); + return parameter; + } + + /// + /// Ensures that the span contains only Unicode decimal digits, or otherwise throws your custom exception. + /// + /// The character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustContainOnlyDigits( + this Span parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + ((ReadOnlySpan) parameter).MustContainOnlyDigits(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only span contains only Unicode decimal digits. Empty spans are valid. + /// + /// The read-only character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character span. + public static ReadOnlySpan MustContainOnlyDigits( + this ReadOnlySpan parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + if (!parameter.ContainsOnlyDigits()) + { + Throw.InvalidStringContent(parameterName, message, "must contain only Unicode decimal digits"); + } + + return parameter; + } + + /// + /// Ensures that the read-only span contains only Unicode decimal digits, or otherwise throws your custom exception. + /// + /// The read-only character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character span. + public static ReadOnlySpan MustContainOnlyDigits( + this ReadOnlySpan parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + if (!parameter.ContainsOnlyDigits()) + { + Throw.CustomSpanException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the memory contains only Unicode decimal digits. Empty memory is valid. + /// + /// The character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustContainOnlyDigits( + this Memory parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter.Span).MustContainOnlyDigits(parameterName, message); + return parameter; + } + + /// + /// Ensures that the memory contains only Unicode decimal digits, or otherwise throws your custom exception. + /// + /// The character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustContainOnlyDigits( + this Memory parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + ((ReadOnlySpan) parameter.Span).MustContainOnlyDigits(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only memory contains only Unicode decimal digits. Empty memory is valid. + /// + /// The read-only character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustContainOnlyDigits( + this ReadOnlyMemory parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + parameter.Span.MustContainOnlyDigits(parameterName, message); + return parameter; + } + + /// + /// Ensures that the read-only memory contains only Unicode decimal digits, or otherwise throws your custom exception. + /// + /// The read-only character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustContainOnlyDigits( + this ReadOnlyMemory parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + parameter.Span.MustContainOnlyDigits(exceptionFactory); + return parameter; + } +} diff --git a/src/Light.GuardClauses/Check.MustContainOnlyLettersOrDigits.cs b/src/Light.GuardClauses/Check.MustContainOnlyLettersOrDigits.cs new file mode 100644 index 00000000..55c3f313 --- /dev/null +++ b/src/Light.GuardClauses/Check.MustContainOnlyLettersOrDigits.cs @@ -0,0 +1,210 @@ +using System; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using Light.GuardClauses.ExceptionFactory; +using Light.GuardClauses.Exceptions; +using NotNullAttribute = System.Diagnostics.CodeAnalysis.NotNullAttribute; + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// + /// Ensures that the string contains only Unicode letters or decimal digits. Empty strings are valid. + /// + /// The string to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated string. + /// Thrown when is null. + /// Thrown when the string contains another character. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static string MustContainOnlyLettersOrDigits( + [NotNull] [ValidatedNotNull] this string? parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + parameter.MustNotBeNull(parameterName, message); + if (!parameter.ContainsOnlyLettersOrDigits()) + { + Throw.InvalidStringContent(parameterName, message, "must contain only Unicode letters or decimal digits"); + } + + return parameter; + } + + /// + /// Ensures that the string contains only Unicode letters or decimal digits, or otherwise throws your custom exception. + /// + /// The string to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated string. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull; exceptionFactory:null => halt")] + public static string MustContainOnlyLettersOrDigits( + [NotNull] [ValidatedNotNull] this string? parameter, + Func exceptionFactory + ) + { + if (parameter is null || !parameter.ContainsOnlyLettersOrDigits()) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the span contains only Unicode letters or decimal digits. Empty spans are valid. + /// + /// The character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustContainOnlyLettersOrDigits( + this Span parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter).MustContainOnlyLettersOrDigits(parameterName, message); + return parameter; + } + + /// + /// Ensures that the span contains only Unicode letters or decimal digits, or otherwise throws your custom exception. + /// + /// The character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustContainOnlyLettersOrDigits( + this Span parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + ((ReadOnlySpan) parameter).MustContainOnlyLettersOrDigits(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only span contains only Unicode letters or decimal digits. Empty spans are valid. + /// + /// The read-only character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character span. + public static ReadOnlySpan MustContainOnlyLettersOrDigits( + this ReadOnlySpan parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + if (!parameter.ContainsOnlyLettersOrDigits()) + { + Throw.InvalidStringContent(parameterName, message, "must contain only Unicode letters or decimal digits"); + } + + return parameter; + } + + /// + /// Ensures that the read-only span contains only Unicode letters or decimal digits, or otherwise throws your custom exception. + /// + /// The read-only character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character span. + public static ReadOnlySpan MustContainOnlyLettersOrDigits( + this ReadOnlySpan parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + if (!parameter.ContainsOnlyLettersOrDigits()) + { + Throw.CustomSpanException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the memory contains only Unicode letters or decimal digits. Empty memory is valid. + /// + /// The character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustContainOnlyLettersOrDigits( + this Memory parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter.Span).MustContainOnlyLettersOrDigits(parameterName, message); + return parameter; + } + + /// + /// Ensures that the memory contains only Unicode letters or decimal digits, or otherwise throws your custom exception. + /// + /// The character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustContainOnlyLettersOrDigits( + this Memory parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + ((ReadOnlySpan) parameter.Span).MustContainOnlyLettersOrDigits(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only memory contains only Unicode letters or decimal digits. Empty memory is valid. + /// + /// The read-only character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustContainOnlyLettersOrDigits( + this ReadOnlyMemory parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + parameter.Span.MustContainOnlyLettersOrDigits(parameterName, message); + return parameter; + } + + /// + /// Ensures that the read-only memory contains only Unicode letters or decimal digits, or otherwise throws your custom exception. + /// + /// The read-only character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustContainOnlyLettersOrDigits( + this ReadOnlyMemory parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + parameter.Span.MustContainOnlyLettersOrDigits(exceptionFactory); + return parameter; + } +} diff --git a/src/Light.GuardClauses/Check.MustNotContainWhiteSpace.cs b/src/Light.GuardClauses/Check.MustNotContainWhiteSpace.cs new file mode 100644 index 00000000..c82e3a5e --- /dev/null +++ b/src/Light.GuardClauses/Check.MustNotContainWhiteSpace.cs @@ -0,0 +1,224 @@ +using System; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using Light.GuardClauses.ExceptionFactory; +using Light.GuardClauses.Exceptions; +using NotNullAttribute = System.Diagnostics.CodeAnalysis.NotNullAttribute; + +namespace Light.GuardClauses; + +public static partial class Check +{ + /// + /// Ensures that the string contains no Unicode whitespace character. Empty strings are valid. + /// + /// The string to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated string. + /// Thrown when is null. + /// Thrown when the string contains a whitespace character. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")] + public static string MustNotContainWhiteSpace( + [NotNull] [ValidatedNotNull] this string? parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + parameter.MustNotBeNull(parameterName, message); + parameter.AsSpan().MustNotContainWhiteSpace(parameterName, message); + return parameter; + } + + /// + /// Ensures that the string contains no Unicode whitespace character, or otherwise throws your custom exception. + /// + /// The string to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated string. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("parameter:null => halt; parameter:notnull => notnull; exceptionFactory:null => halt")] + public static string MustNotContainWhiteSpace( + [NotNull] [ValidatedNotNull] this string? parameter, + Func exceptionFactory + ) + { + if (parameter is null) + { + Throw.CustomException(exceptionFactory, parameter); + } + + if (ContainsWhiteSpace(parameter.AsSpan())) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the span contains no Unicode whitespace character. Empty spans are valid. + /// + /// The character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustNotContainWhiteSpace( + this Span parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter).MustNotContainWhiteSpace(parameterName, message); + return parameter; + } + + /// + /// Ensures that the span contains no Unicode whitespace character, or otherwise throws your custom exception. + /// + /// The character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character span. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustNotContainWhiteSpace( + this Span parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + ((ReadOnlySpan) parameter).MustNotContainWhiteSpace(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only span contains no Unicode whitespace character. Empty spans are valid. + /// + /// The read-only character span to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character span. + public static ReadOnlySpan MustNotContainWhiteSpace( + this ReadOnlySpan parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + if (ContainsWhiteSpace(parameter)) + { + Throw.InvalidStringContent(parameterName, message, "must contain no Unicode whitespace characters"); + } + + return parameter; + } + + /// + /// Ensures that the read-only span contains no Unicode whitespace character, or otherwise throws your custom exception. + /// + /// The read-only character span to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character span. + public static ReadOnlySpan MustNotContainWhiteSpace( + this ReadOnlySpan parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + if (ContainsWhiteSpace(parameter)) + { + Throw.CustomSpanException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the memory contains no Unicode whitespace character. Empty memory is valid. + /// + /// The character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustNotContainWhiteSpace( + this Memory parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter.Span).MustNotContainWhiteSpace(parameterName, message); + return parameter; + } + + /// + /// Ensures that the memory contains no Unicode whitespace character, or otherwise throws your custom exception. + /// + /// The character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustNotContainWhiteSpace( + this Memory parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + ((ReadOnlySpan) parameter.Span).MustNotContainWhiteSpace(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the read-only memory contains no Unicode whitespace character. Empty memory is valid. + /// + /// The read-only character memory to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustNotContainWhiteSpace( + this ReadOnlyMemory parameter, + [CallerArgumentExpression(nameof(parameter))] string? parameterName = null, + string? message = null + ) + { + parameter.Span.MustNotContainWhiteSpace(parameterName, message); + return parameter; + } + + /// + /// Ensures that the read-only memory contains no Unicode whitespace character, or otherwise throws your custom exception. + /// + /// The read-only character memory to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// The validated read-only character memory. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustNotContainWhiteSpace( + this ReadOnlyMemory parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + parameter.Span.MustNotContainWhiteSpace(exceptionFactory); + return parameter; + } + + private static bool ContainsWhiteSpace(ReadOnlySpan parameter) + { + foreach (var character in parameter) + { + if (char.IsWhiteSpace(character)) + { + return true; + } + } + + return false; + } +} diff --git a/src/Light.GuardClauses/ExceptionFactory/Throw.InvalidStringContent.cs b/src/Light.GuardClauses/ExceptionFactory/Throw.InvalidStringContent.cs new file mode 100644 index 00000000..0fee7753 --- /dev/null +++ b/src/Light.GuardClauses/ExceptionFactory/Throw.InvalidStringContent.cs @@ -0,0 +1,22 @@ +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; +using Light.GuardClauses.Exceptions; + +namespace Light.GuardClauses.ExceptionFactory; + +public static partial class Throw +{ + /// + /// Throws a for character content that violates a required invariant. + /// + /// The name of the parameter that contains the invalid string (optional). + /// The message that will be passed to the exception (optional). + /// A description of the requirement that the string violates. + [ContractAnnotation("=> halt")] + [DoesNotReturn] + public static void InvalidStringContent(string? parameterName, string? message, string requirement) => + throw new StringException( + parameterName, + message ?? $"{parameterName ?? "The string"} {requirement}." + ); +} diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 4f061c3e..755191d7 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -3,12 +3,16 @@ true + Exe + true + true - - - + + + + diff --git a/tests/Light.GuardClauses.SourceCodeTransformation.Tests/ParseCSharpFilesWithRoslynTests.cs b/tests/Light.GuardClauses.SourceCodeTransformation.Tests/ParseCSharpFilesWithRoslynTests.cs index f047be66..7cc6f08e 100644 --- a/tests/Light.GuardClauses.SourceCodeTransformation.Tests/ParseCSharpFilesWithRoslynTests.cs +++ b/tests/Light.GuardClauses.SourceCodeTransformation.Tests/ParseCSharpFilesWithRoslynTests.cs @@ -12,8 +12,11 @@ public static class ParseCSharpWithRoslynTests public static void ParseExpressionExtensionsFile() { var fileInfo = GetLightGuardClausesFile("FrameworkExtensions/ExpressionExtensions.cs"); - var syntaxTree = CSharpSyntaxTree.ParseText(File.ReadAllText(fileInfo.FullName)); - var root = (CompilationUnitSyntax) syntaxTree.GetRoot(); + var syntaxTree = CSharpSyntaxTree.ParseText( + File.ReadAllText(fileInfo.FullName), + cancellationToken: TestContext.Current.CancellationToken + ); + var root = (CompilationUnitSyntax) syntaxTree.GetRoot(TestContext.Current.CancellationToken); root.Members.Should().NotBeEmpty(); } @@ -24,9 +27,10 @@ public static void ParseSpanDelegatesFile() var fileInfo = GetLightGuardClausesFile("ExceptionFactory/SpanDelegates.cs"); var syntaxTree = CSharpSyntaxTree.ParseText( File.ReadAllText(fileInfo.FullName), - new (LanguageVersion.CSharp7_3, preprocessorSymbols: ["NETSTANDARD2_0"]) + new (LanguageVersion.CSharp7_3, preprocessorSymbols: ["NETSTANDARD2_0"]), + cancellationToken: TestContext.Current.CancellationToken ); - var root = (CompilationUnitSyntax) syntaxTree.GetRoot(); + var root = (CompilationUnitSyntax) syntaxTree.GetRoot(TestContext.Current.CancellationToken); root.Members.Should().NotBeEmpty(); } diff --git a/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerFrameworkTests.cs b/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerFrameworkTests.cs index 3169625e..1b9bb2c1 100644 --- a/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerFrameworkTests.cs +++ b/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerFrameworkTests.cs @@ -17,9 +17,12 @@ public static void Net10ExportContainsModernMembersAndNoDirectives() var sourceCode = File.ReadAllText(targetFile); sourceCode.Should().Contain("using System.Numerics;"); + sourceCode.Should().Contain("using System.Buffers.Text;"); sourceCode.Should().Contain("INumber"); sourceCode.Should().Contain("IFloatingPointIeee754"); sourceCode.Should().Contain("Ascii.IsValid(parameter)"); + sourceCode.Should().Contain("Base64.IsValid(parameter)"); + sourceCode.Should().Contain("char.IsAsciiHexDigit(character)"); sourceCode.Should().Contain("MustBeGreaterThanOrApproximately"); sourceCode.Should().Contain("public static ReadOnlySpan MustBeEmailAddress"); sourceCode.Should().Contain("public static Span MustBeEmailAddress"); @@ -37,9 +40,14 @@ public static void NetStandardExportOmitsModernMembersAndNoDirectives() var sourceCode = File.ReadAllText(targetFile); sourceCode.Should().NotContain("using System.Numerics;"); + sourceCode.Should().NotContain("using System.Buffers.Text;"); sourceCode.Should().NotContain("INumber"); sourceCode.Should().NotContain("IFloatingPointIeee754"); sourceCode.Should().NotContain("Ascii.IsValid(parameter)"); + sourceCode.Should().NotContain("Base64.IsValid(parameter)"); + sourceCode.Should().NotContain("char.IsAsciiHexDigit(character)"); + sourceCode.Should().Contain("IsBase64Portable(parameter)"); + sourceCode.Should().Contain("private static bool IsAsciiHexDigit(char character)"); sourceCode.Should().NotContain("ReadOnlySpan MustBeEmailAddress"); sourceCode.Should().NotContain("Span MustBeEmailAddress"); sourceCode.Should().NotContain("Memory MustBeEmailAddress"); @@ -118,7 +126,7 @@ public static partial class Check TargetFile = targetFile, TargetFramework = SourceTargetFramework.NetStandard2_0, IncludeVersionComment = false, - AssertionWhitelist = new() { IsEnabled = true }, + AssertionWhitelist = new () { IsEnabled = true }, }; var act = () => SourceFileMerger.CreateSingleSourceFile(options); diff --git a/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerWhitelistTests.cs b/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerWhitelistTests.cs index 83ca11f5..c59799bd 100644 --- a/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerWhitelistTests.cs +++ b/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerWhitelistTests.cs @@ -339,6 +339,69 @@ public static void MustNotContainNullOrWhiteSpaceWhitelistExportsDependenciesAnd sourceCode.Should().NotContain("MustNotContainNull"); } + [Fact] + public static void StringInspectionWhitelistsRetainPredicatesHelpersAndPortableBase64() + { + using var temporaryDirectory = new TemporaryDirectory(); + var targetFile = Path.Combine(temporaryDirectory.DirectoryPath, "StringInspection.cs"); + + SourceFileMerger.CreateSingleSourceFile( + CreateOptions( + targetFile, + CreateWhitelist( + includedAssertions: + [ + new ("MustContainOnlyDigits", true), + new ("MustContainOnlyLettersOrDigits", true), + new ("MustBeUpperCase", true), + new ("MustBeLowerCase", true), + new ("MustBeBase64", true), + new ("MustBeHexadecimal", true), + new ("MustNotContainWhiteSpace", true), + ] + ) + ) + ); + var sourceCode = File.ReadAllText(targetFile); + + sourceCode.Should().Contain("ContainsOnlyDigits(this ReadOnlySpan parameter)"); + sourceCode.Should().Contain("ContainsOnlyLettersOrDigits(this ReadOnlySpan parameter)"); + sourceCode.Should().Contain("IsUpperCase(this ReadOnlySpan parameter)"); + sourceCode.Should().Contain("IsLowerCase(this ReadOnlySpan parameter)"); + sourceCode.Should().Contain("IsBase64Portable(ReadOnlySpan parameter)"); + sourceCode.Should().Contain("IsHexadecimal(this ReadOnlySpan parameter)"); + sourceCode.Should().Contain("ContainsWhiteSpace(ReadOnlySpan parameter)"); + sourceCode.Should().Contain("public static void InvalidStringContent("); + sourceCode.Should().Contain("class StringException : ArgumentException"); + sourceCode.Should().Contain("delegate Exception ReadOnlySpanExceptionFactory"); + sourceCode.Should().NotContain("Base64.IsValid(parameter)"); + sourceCode.Should().NotContain("MustBeAscii("); + } + + [Fact] + public static void StringInspectionWhitelistTrimsExceptionFactories() + { + using var temporaryDirectory = new TemporaryDirectory(); + var targetFile = Path.Combine(temporaryDirectory.DirectoryPath, "StringInspectionWithoutFactories.cs"); + + SourceFileMerger.CreateSingleSourceFile( + CreateOptions( + targetFile, + CreateWhitelist( + includedAssertions: [new ("MustBeBase64", false)] + ) + ) + ); + var sourceCode = File.ReadAllText(targetFile); + + sourceCode.Should().Contain("MustBeBase64("); + sourceCode.Should().Contain("IsBase64Portable(ReadOnlySpan parameter)"); + sourceCode.Should().Contain("public static void InvalidStringContent("); + sourceCode.Should().NotContain("Func exceptionFactory"); + sourceCode.Should().NotContain("ReadOnlySpanExceptionFactory exceptionFactory"); + sourceCode.Should().NotContain("public static void CustomSpanException"); + } + private static SourceFileMergeOptions CreateOptions( string targetFile, AssertionWhitelist assertionWhitelist = null, diff --git a/tests/Light.GuardClauses.Tests/DefaultVariablesData.cs b/tests/Light.GuardClauses.Tests/DefaultVariablesData.cs index 4063b6a6..be238844 100644 --- a/tests/Light.GuardClauses.Tests/DefaultVariablesData.cs +++ b/tests/Light.GuardClauses.Tests/DefaultVariablesData.cs @@ -1,7 +1,10 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Threading.Tasks; +using Xunit; using Xunit.Sdk; +using Xunit.v3; namespace Light.GuardClauses.Tests; @@ -22,15 +25,20 @@ public sealed class DefaultVariablesData : DataAttribute "Fred", "Plugh", "Xyzzy", - "Thud" + "Thud", }; - private static readonly List TransformedVariables = - All.Select(variable => new object[] { variable }).ToList(); + private static readonly IReadOnlyCollection TransformedVariables = + All.Select(ITheoryDataRow (variable) => new TheoryDataRow(variable)).ToArray(); private readonly int _numberOfConstants; public DefaultVariablesData(int numberOfConstants = 3) => _numberOfConstants = numberOfConstants; - public override IEnumerable GetData(MethodInfo testMethod) => TransformedVariables.Take(_numberOfConstants); -} \ No newline at end of file + public override ValueTask> GetData( + MethodInfo testMethod, + DisposalTracker disposalTracker + ) => new (TransformedVariables.Take(_numberOfConstants).ToArray()); + + public override bool SupportsDiscoveryEnumeration() => true; +} diff --git a/tests/Light.GuardClauses.Tests/StringAssertions/AsciiTests.cs b/tests/Light.GuardClauses.Tests/StringAssertions/AsciiTests.cs index e4ec5701..6182c8ea 100644 --- a/tests/Light.GuardClauses.Tests/StringAssertions/AsciiTests.cs +++ b/tests/Light.GuardClauses.Tests/StringAssertions/AsciiTests.cs @@ -1,5 +1,6 @@ using System; using FluentAssertions; +using Light.GuardClauses.Exceptions; using Xunit; namespace Light.GuardClauses.Tests.StringAssertions; @@ -75,13 +76,16 @@ public static void ScalarAndStringFailuresSupportDefaultMessagesFactoriesAndNull { const char invalidCharacter = 'é'; const byte invalidByte = 128; + const string invalidText = "é"; string nullText = null; - ((Action) (() => invalidCharacter.MustBeAscii())).Should().Throw() + ((Action) (() => invalidCharacter.MustBeAscii())).Should().ThrowExactly() .WithParameterName(nameof(invalidCharacter)); - ((Action) (() => invalidByte.MustBeAscii(message: "custom"))).Should().Throw() + ((Action) (() => invalidByte.MustBeAscii(message: "custom"))).Should().ThrowExactly() .WithParameterName(nameof(invalidByte)) .WithMessage("*custom*"); + ((Action) (() => invalidText.MustBeAscii())).Should().ThrowExactly() + .WithParameterName(nameof(invalidText)); ((Action) (() => nullText.MustBeAscii())).Should().Throw() .WithParameterName(nameof(nullText)); Test.CustomException(invalidCharacter, (value, factory) => value.MustBeAscii(factory)); @@ -126,14 +130,24 @@ public static void BufferDefaultFailuresCaptureExpressionsAndMessages() ReadOnlySpan invalidByteSpan = new byte[] { 128 }; invalidByteSpan.MustBeAscii(message: "custom"); }; + var readOnlyCharacterSpanAct = () => + { + ReadOnlySpan invalidCharacterSpan = "é"; + invalidCharacterSpan.MustBeAscii(); + }; var invalidCharacterMemory = "é".ToCharArray().AsMemory(); + ReadOnlyMemory invalidReadOnlyCharacterMemory = "é".ToCharArray(); ReadOnlyMemory invalidByteMemory = new byte[] { 128 }; - spanAct.Should().Throw().WithParameterName("invalidCharacterSpan"); - readOnlySpanAct.Should().Throw().WithParameterName("invalidByteSpan").WithMessage("*custom*"); - ((Action) (() => invalidCharacterMemory.MustBeAscii())).Should().Throw() + spanAct.Should().ThrowExactly().WithParameterName("invalidCharacterSpan"); + readOnlyCharacterSpanAct.Should().ThrowExactly() + .WithParameterName("invalidCharacterSpan"); + readOnlySpanAct.Should().ThrowExactly().WithParameterName("invalidByteSpan").WithMessage("*custom*"); + ((Action) (() => invalidCharacterMemory.MustBeAscii())).Should().ThrowExactly() .WithParameterName(nameof(invalidCharacterMemory)); - ((Action) (() => invalidByteMemory.MustBeAscii())).Should().Throw() + ((Action) (() => invalidReadOnlyCharacterMemory.MustBeAscii())).Should().ThrowExactly() + .WithParameterName(nameof(invalidReadOnlyCharacterMemory)); + ((Action) (() => invalidByteMemory.MustBeAscii())).Should().ThrowExactly() .WithParameterName(nameof(invalidByteMemory)); } diff --git a/tests/Light.GuardClauses.Tests/StringAssertions/OrdinalIgnoreCaseIgnoreWhiteSpaceComparerTests.cs b/tests/Light.GuardClauses.Tests/StringAssertions/OrdinalIgnoreCaseIgnoreWhiteSpaceComparerTests.cs index b95331d3..40c2f4e0 100644 --- a/tests/Light.GuardClauses.Tests/StringAssertions/OrdinalIgnoreCaseIgnoreWhiteSpaceComparerTests.cs +++ b/tests/Light.GuardClauses.Tests/StringAssertions/OrdinalIgnoreCaseIgnoreWhiteSpaceComparerTests.cs @@ -1,7 +1,6 @@ using System; using FluentAssertions; using Xunit; -using Xunit.Abstractions; namespace Light.GuardClauses.Tests.StringAssertions; @@ -10,7 +9,7 @@ public sealed class OrdinalIgnoreCaseIgnoreWhiteSpaceComparerTests private static readonly OrdinalIgnoreCaseIgnoreWhiteSpaceComparer Comparer = new (); private readonly ITestOutputHelper _output; - public OrdinalIgnoreCaseIgnoreWhiteSpaceComparerTests(ITestOutputHelper output) => _output = output.MustNotBeNull(nameof(output)); + public OrdinalIgnoreCaseIgnoreWhiteSpaceComparerTests(ITestOutputHelper output) => _output = output.MustNotBeNull(); [Theory] [InlineData("Foo", "FOO", true)] @@ -49,4 +48,4 @@ public void GetHashCodeStringNull() act.Should().Throw().WriteExceptionTo(_output); } -} \ No newline at end of file +} diff --git a/tests/Light.GuardClauses.Tests/StringAssertions/OrdinalIgnoreWhiteSpaceComparerTests.cs b/tests/Light.GuardClauses.Tests/StringAssertions/OrdinalIgnoreWhiteSpaceComparerTests.cs index 19021114..7b73b1d5 100644 --- a/tests/Light.GuardClauses.Tests/StringAssertions/OrdinalIgnoreWhiteSpaceComparerTests.cs +++ b/tests/Light.GuardClauses.Tests/StringAssertions/OrdinalIgnoreWhiteSpaceComparerTests.cs @@ -1,7 +1,6 @@ using System; using FluentAssertions; using Xunit; -using Xunit.Abstractions; namespace Light.GuardClauses.Tests.StringAssertions; @@ -10,7 +9,7 @@ public sealed class OrdinalIgnoreWhiteSpaceComparerTests private static readonly OrdinalIgnoreWhiteSpaceComparer Comparer = new (); private readonly ITestOutputHelper _output; - public OrdinalIgnoreWhiteSpaceComparerTests(ITestOutputHelper output) => _output = output.MustNotBeNull(nameof(output)); + public OrdinalIgnoreWhiteSpaceComparerTests(ITestOutputHelper output) => _output = output.MustNotBeNull(); [Theory] [InlineData("Foo", " Foo", true)] @@ -41,7 +40,7 @@ public void EqualsStringNull(string x, string y) [InlineData("Foo", "Foo")] [InlineData(" Bar", "Bar ")] [InlineData("{\r\n\t\"foo\": 42\r\n}\r\n", "{\"foo\": 42}")] - public static void CheckGetHashCode(string x, string y) => + public static void CheckGetHashCode(string x, string y) => Comparer.GetHashCode(x).Should().Be(Comparer.GetHashCode(y)); [Fact] @@ -52,4 +51,4 @@ public void GetHashCodeStringNull() act.Should().Throw().WriteExceptionTo(_output); } -} \ No newline at end of file +} diff --git a/tests/Light.GuardClauses.Tests/StringAssertions/StringInspectionGuardTests.cs b/tests/Light.GuardClauses.Tests/StringAssertions/StringInspectionGuardTests.cs new file mode 100644 index 00000000..92d994df --- /dev/null +++ b/tests/Light.GuardClauses.Tests/StringAssertions/StringInspectionGuardTests.cs @@ -0,0 +1,399 @@ +using System; +using FluentAssertions; +using Light.GuardClauses.ExceptionFactory; +using Light.GuardClauses.Exceptions; +using Xunit; + +namespace Light.GuardClauses.Tests.StringAssertions; + +public static class StringInspectionGuardTests +{ + [Fact] + public static void EveryGuardReturnsEveryOriginalReceiverShape() + { + AssertDigitReturns("1٢3"); + AssertLetterOrDigitReturns("AzΩ٢"); + AssertUpperCaseReturns("AΩ 123!"); + AssertLowerCaseReturns("aω 123!"); + AssertBase64Returns("T W\tF\ru\n"); + AssertHexadecimalReturns("09aAfF"); + AssertNoWhiteSpaceReturns("Az-09"); + + string.Empty.MustContainOnlyDigits().Should().BeEmpty(); + Span.Empty.MustContainOnlyLettersOrDigits().IsEmpty.Should().BeTrue(); + ReadOnlySpan.Empty.MustBeUpperCase().IsEmpty.Should().BeTrue(); + Memory.Empty.MustBeLowerCase().IsEmpty.Should().BeTrue(); + ReadOnlyMemory.Empty.MustBeBase64().IsEmpty.Should().BeTrue(); + Span.Empty.MustBeHexadecimal().IsEmpty.Should().BeTrue(); + ReadOnlySpan.Empty.MustNotContainWhiteSpace().IsEmpty.Should().BeTrue(); + } + + [Fact] + public static void EveryFactoryGuardReturnsEveryOriginalReceiverShape() + { + AssertEveryFactoryShape("1٢3", GuardFamily.Digits); + AssertEveryFactoryShape("AzΩ٢", GuardFamily.LettersOrDigits); + AssertEveryFactoryShape("AΩ 123!", GuardFamily.UpperCase); + AssertEveryFactoryShape("aω 123!", GuardFamily.LowerCase); + AssertEveryFactoryShape("T W\tF\ru\n", GuardFamily.Base64); + AssertEveryFactoryShape("09aAfF", GuardFamily.Hexadecimal); + AssertEveryFactoryShape("Az-09", GuardFamily.NoWhiteSpace); + } + + [Fact] + public static void NullStringsThrowArgumentNullExceptionForEveryDefaultGuard() + { + string nullText = null; + + ((Action) (() => nullText.MustContainOnlyDigits())).Should().ThrowExactly() + .WithParameterName(nameof(nullText)); + ((Action) (() => nullText.MustContainOnlyLettersOrDigits())).Should().ThrowExactly(); + ((Action) (() => nullText.MustBeUpperCase())).Should().ThrowExactly(); + ((Action) (() => nullText.MustBeLowerCase())).Should().ThrowExactly(); + ((Action) (() => nullText.MustBeBase64())).Should().ThrowExactly(); + ((Action) (() => nullText.MustBeHexadecimal())).Should().ThrowExactly(); + ((Action) (() => nullText.MustNotContainWhiteSpace())).Should().ThrowExactly(); + } + + [Fact] + public static void InvalidStringContentThrowsStringExceptionForEveryGuard() + { + AssertStringFailure("a", value => value.MustContainOnlyDigits()); + AssertStringFailure("!", value => value.MustContainOnlyLettersOrDigits()); + AssertStringFailure("a", value => value.MustBeUpperCase()); + AssertStringFailure("A", value => value.MustBeLowerCase()); + AssertStringFailure("_", value => value.MustBeBase64()); + AssertStringFailure("g", value => value.MustBeHexadecimal()); + AssertStringFailure(" ", value => value.MustNotContainWhiteSpace()); + } + + [Fact] + public static void DefaultBufferFailuresCaptureExpressionsAndSupportCustomMessages() + { + var spanAct = () => + { + var invalidSpan = "a".ToCharArray().AsSpan(); + invalidSpan.MustContainOnlyDigits(); + }; + var readOnlySpanAct = () => + { + ReadOnlySpan invalidReadOnlySpan = "a"; + invalidReadOnlySpan.MustContainOnlyDigits(message: "custom"); + }; + var invalidMemory = "a".ToCharArray().AsMemory(); + ReadOnlyMemory invalidReadOnlyMemory = "a".ToCharArray(); + + spanAct.Should().ThrowExactly().WithParameterName("invalidSpan"); + readOnlySpanAct.Should().ThrowExactly() + .WithParameterName("invalidReadOnlySpan") + .WithMessage("*custom*"); + ((Action) (() => invalidMemory.MustContainOnlyDigits())).Should().ThrowExactly() + .WithParameterName(nameof(invalidMemory)); + ((Action) (() => invalidReadOnlyMemory.MustContainOnlyDigits())).Should().ThrowExactly() + .WithParameterName( + nameof(invalidReadOnlyMemory) + ); + } + + [Fact] + public static void EveryStringFactoryReceivesNullAndInvalidContent() + { + string nullText = null; + Test.CustomException(nullText, (value, factory) => value.MustContainOnlyDigits(factory)); + Test.CustomException("a", (value, factory) => value.MustContainOnlyDigits(factory)); + Test.CustomException("!", (value, factory) => value.MustContainOnlyLettersOrDigits(factory)); + Test.CustomException("a", (value, factory) => value.MustBeUpperCase(factory)); + Test.CustomException("A", (value, factory) => value.MustBeLowerCase(factory)); + Test.CustomException("_", (value, factory) => value.MustBeBase64(factory)); + Test.CustomException("g", (value, factory) => value.MustBeHexadecimal(factory)); + Test.CustomException(" ", (value, factory) => value.MustNotContainWhiteSpace(factory)); + } + + [Fact] + public static void EveryBufferFactoryReceivesEveryInvalidReceiverShape() + { + AssertEveryBufferFactoryFailure("a", GuardFamily.Digits); + AssertEveryBufferFactoryFailure("!", GuardFamily.LettersOrDigits); + AssertEveryBufferFactoryFailure("a", GuardFamily.UpperCase); + AssertEveryBufferFactoryFailure("A", GuardFamily.LowerCase); + AssertEveryBufferFactoryFailure("_", GuardFamily.Base64); + AssertEveryBufferFactoryFailure("g", GuardFamily.Hexadecimal); + AssertEveryBufferFactoryFailure(" ", GuardFamily.NoWhiteSpace); + } + + [Fact] + public static void WhiteSpaceGuardUsesUnicodeClassificationAtEveryPosition() + { + "\u00A0abc".Invoking(value => value.MustNotContainWhiteSpace()).Should().ThrowExactly(); + "ab\u2003cd".Invoking(value => value.MustNotContainWhiteSpace()).Should().ThrowExactly(); + "abc\u2029".Invoking(value => value.MustNotContainWhiteSpace()).Should().ThrowExactly(); + "abc-def".MustNotContainWhiteSpace().Should().Be("abc-def"); + } + + private static void AssertDigitReturns(string value) => AssertEveryShape(value, GuardFamily.Digits); + + private static void AssertLetterOrDigitReturns(string value) => + AssertEveryShape(value, GuardFamily.LettersOrDigits); + + private static void AssertUpperCaseReturns(string value) => AssertEveryShape(value, GuardFamily.UpperCase); + + private static void AssertLowerCaseReturns(string value) => AssertEveryShape(value, GuardFamily.LowerCase); + + private static void AssertBase64Returns(string value) => AssertEveryShape(value, GuardFamily.Base64); + + private static void AssertHexadecimalReturns(string value) => AssertEveryShape(value, GuardFamily.Hexadecimal); + + private static void AssertNoWhiteSpaceReturns(string value) => AssertEveryShape(value, GuardFamily.NoWhiteSpace); + + private static void AssertEveryShape(string value, GuardFamily family) + { + var characters = value.ToCharArray(); + var span = characters.AsSpan(); + ReadOnlySpan readOnlySpan = characters; + var memory = characters.AsMemory(); + ReadOnlyMemory readOnlyMemory = characters; + + switch (family) + { + case GuardFamily.Digits: + value.MustContainOnlyDigits().Should().BeSameAs(value); + (span.MustContainOnlyDigits() == span).Should().BeTrue(); + (readOnlySpan.MustContainOnlyDigits() == readOnlySpan).Should().BeTrue(); + memory.MustContainOnlyDigits().Should().Be(memory); + readOnlyMemory.MustContainOnlyDigits().Should().Be(readOnlyMemory); + break; + case GuardFamily.LettersOrDigits: + value.MustContainOnlyLettersOrDigits().Should().BeSameAs(value); + (span.MustContainOnlyLettersOrDigits() == span).Should().BeTrue(); + (readOnlySpan.MustContainOnlyLettersOrDigits() == readOnlySpan).Should().BeTrue(); + memory.MustContainOnlyLettersOrDigits().Should().Be(memory); + readOnlyMemory.MustContainOnlyLettersOrDigits().Should().Be(readOnlyMemory); + break; + case GuardFamily.UpperCase: + value.MustBeUpperCase().Should().BeSameAs(value); + (span.MustBeUpperCase() == span).Should().BeTrue(); + (readOnlySpan.MustBeUpperCase() == readOnlySpan).Should().BeTrue(); + memory.MustBeUpperCase().Should().Be(memory); + readOnlyMemory.MustBeUpperCase().Should().Be(readOnlyMemory); + break; + case GuardFamily.LowerCase: + value.MustBeLowerCase().Should().BeSameAs(value); + (span.MustBeLowerCase() == span).Should().BeTrue(); + (readOnlySpan.MustBeLowerCase() == readOnlySpan).Should().BeTrue(); + memory.MustBeLowerCase().Should().Be(memory); + readOnlyMemory.MustBeLowerCase().Should().Be(readOnlyMemory); + break; + case GuardFamily.Base64: + value.MustBeBase64().Should().BeSameAs(value); + (span.MustBeBase64() == span).Should().BeTrue(); + (readOnlySpan.MustBeBase64() == readOnlySpan).Should().BeTrue(); + memory.MustBeBase64().Should().Be(memory); + readOnlyMemory.MustBeBase64().Should().Be(readOnlyMemory); + break; + case GuardFamily.Hexadecimal: + value.MustBeHexadecimal().Should().BeSameAs(value); + (span.MustBeHexadecimal() == span).Should().BeTrue(); + (readOnlySpan.MustBeHexadecimal() == readOnlySpan).Should().BeTrue(); + memory.MustBeHexadecimal().Should().Be(memory); + readOnlyMemory.MustBeHexadecimal().Should().Be(readOnlyMemory); + break; + case GuardFamily.NoWhiteSpace: + value.MustNotContainWhiteSpace().Should().BeSameAs(value); + (span.MustNotContainWhiteSpace() == span).Should().BeTrue(); + (readOnlySpan.MustNotContainWhiteSpace() == readOnlySpan).Should().BeTrue(); + memory.MustNotContainWhiteSpace().Should().Be(memory); + readOnlyMemory.MustNotContainWhiteSpace().Should().Be(readOnlyMemory); + break; + default: throw new ArgumentOutOfRangeException(nameof(family), family, null); + } + } + + private static void AssertEveryFactoryShape(string value, GuardFamily family) + { + var characters = value.ToCharArray(); + var span = characters.AsSpan(); + ReadOnlySpan readOnlySpan = characters; + var memory = characters.AsMemory(); + ReadOnlyMemory readOnlyMemory = characters; + Func stringFactory = _ => new ("The factory should not be invoked."); + ReadOnlySpanExceptionFactory bufferFactory = _ => new ("The factory should not be invoked."); + + switch (family) + { + case GuardFamily.Digits: + value.MustContainOnlyDigits(stringFactory).Should().BeSameAs(value); + (span.MustContainOnlyDigits(bufferFactory) == span).Should().BeTrue(); + (readOnlySpan.MustContainOnlyDigits(bufferFactory) == readOnlySpan).Should().BeTrue(); + memory.MustContainOnlyDigits(bufferFactory).Should().Be(memory); + readOnlyMemory.MustContainOnlyDigits(bufferFactory).Should().Be(readOnlyMemory); + break; + case GuardFamily.LettersOrDigits: + value.MustContainOnlyLettersOrDigits(stringFactory).Should().BeSameAs(value); + (span.MustContainOnlyLettersOrDigits(bufferFactory) == span).Should().BeTrue(); + (readOnlySpan.MustContainOnlyLettersOrDigits(bufferFactory) == readOnlySpan).Should().BeTrue(); + memory.MustContainOnlyLettersOrDigits(bufferFactory).Should().Be(memory); + readOnlyMemory.MustContainOnlyLettersOrDigits(bufferFactory).Should().Be(readOnlyMemory); + break; + case GuardFamily.UpperCase: + value.MustBeUpperCase(stringFactory).Should().BeSameAs(value); + (span.MustBeUpperCase(bufferFactory) == span).Should().BeTrue(); + (readOnlySpan.MustBeUpperCase(bufferFactory) == readOnlySpan).Should().BeTrue(); + memory.MustBeUpperCase(bufferFactory).Should().Be(memory); + readOnlyMemory.MustBeUpperCase(bufferFactory).Should().Be(readOnlyMemory); + break; + case GuardFamily.LowerCase: + value.MustBeLowerCase(stringFactory).Should().BeSameAs(value); + (span.MustBeLowerCase(bufferFactory) == span).Should().BeTrue(); + (readOnlySpan.MustBeLowerCase(bufferFactory) == readOnlySpan).Should().BeTrue(); + memory.MustBeLowerCase(bufferFactory).Should().Be(memory); + readOnlyMemory.MustBeLowerCase(bufferFactory).Should().Be(readOnlyMemory); + break; + case GuardFamily.Base64: + value.MustBeBase64(stringFactory).Should().BeSameAs(value); + (span.MustBeBase64(bufferFactory) == span).Should().BeTrue(); + (readOnlySpan.MustBeBase64(bufferFactory) == readOnlySpan).Should().BeTrue(); + memory.MustBeBase64(bufferFactory).Should().Be(memory); + readOnlyMemory.MustBeBase64(bufferFactory).Should().Be(readOnlyMemory); + break; + case GuardFamily.Hexadecimal: + value.MustBeHexadecimal(stringFactory).Should().BeSameAs(value); + (span.MustBeHexadecimal(bufferFactory) == span).Should().BeTrue(); + (readOnlySpan.MustBeHexadecimal(bufferFactory) == readOnlySpan).Should().BeTrue(); + memory.MustBeHexadecimal(bufferFactory).Should().Be(memory); + readOnlyMemory.MustBeHexadecimal(bufferFactory).Should().Be(readOnlyMemory); + break; + case GuardFamily.NoWhiteSpace: + value.MustNotContainWhiteSpace(stringFactory).Should().BeSameAs(value); + (span.MustNotContainWhiteSpace(bufferFactory) == span).Should().BeTrue(); + (readOnlySpan.MustNotContainWhiteSpace(bufferFactory) == readOnlySpan).Should().BeTrue(); + memory.MustNotContainWhiteSpace(bufferFactory).Should().Be(memory); + readOnlyMemory.MustNotContainWhiteSpace(bufferFactory).Should().Be(readOnlyMemory); + break; + default: throw new ArgumentOutOfRangeException(nameof(family), family, null); + } + } + + private static void AssertEveryBufferFactoryFailure(string invalidValue, GuardFamily family) + { + var characters = invalidValue.ToCharArray(); + + switch (family) + { + case GuardFamily.Digits: + Test.CustomSpanException(characters.AsSpan(), (value, factory) => value.MustContainOnlyDigits(factory)); + Test.CustomSpanException( + (ReadOnlySpan) characters, + (value, factory) => value.MustContainOnlyDigits(factory) + ); + Test.CustomMemoryException( + characters.AsMemory(), + (value, factory) => value.MustContainOnlyDigits(factory) + ); + Test.CustomMemoryException( + (ReadOnlyMemory) characters, + (value, factory) => value.MustContainOnlyDigits(factory) + ); + break; + case GuardFamily.LettersOrDigits: + Test.CustomSpanException( + characters.AsSpan(), + (value, factory) => value.MustContainOnlyLettersOrDigits(factory) + ); + Test.CustomSpanException( + (ReadOnlySpan) characters, + (value, factory) => value.MustContainOnlyLettersOrDigits(factory) + ); + Test.CustomMemoryException( + characters.AsMemory(), + (value, factory) => value.MustContainOnlyLettersOrDigits(factory) + ); + Test.CustomMemoryException( + (ReadOnlyMemory) characters, + (value, factory) => value.MustContainOnlyLettersOrDigits(factory) + ); + break; + case GuardFamily.UpperCase: + Test.CustomSpanException(characters.AsSpan(), (value, factory) => value.MustBeUpperCase(factory)); + Test.CustomSpanException( + (ReadOnlySpan) characters, + (value, factory) => value.MustBeUpperCase(factory) + ); + Test.CustomMemoryException(characters.AsMemory(), (value, factory) => value.MustBeUpperCase(factory)); + Test.CustomMemoryException( + (ReadOnlyMemory) characters, + (value, factory) => value.MustBeUpperCase(factory) + ); + break; + case GuardFamily.LowerCase: + Test.CustomSpanException(characters.AsSpan(), (value, factory) => value.MustBeLowerCase(factory)); + Test.CustomSpanException( + (ReadOnlySpan) characters, + (value, factory) => value.MustBeLowerCase(factory) + ); + Test.CustomMemoryException(characters.AsMemory(), (value, factory) => value.MustBeLowerCase(factory)); + Test.CustomMemoryException( + (ReadOnlyMemory) characters, + (value, factory) => value.MustBeLowerCase(factory) + ); + break; + case GuardFamily.Base64: + Test.CustomSpanException(characters.AsSpan(), (value, factory) => value.MustBeBase64(factory)); + Test.CustomSpanException( + (ReadOnlySpan) characters, + (value, factory) => value.MustBeBase64(factory) + ); + Test.CustomMemoryException(characters.AsMemory(), (value, factory) => value.MustBeBase64(factory)); + Test.CustomMemoryException( + (ReadOnlyMemory) characters, + (value, factory) => value.MustBeBase64(factory) + ); + break; + case GuardFamily.Hexadecimal: + Test.CustomSpanException(characters.AsSpan(), (value, factory) => value.MustBeHexadecimal(factory)); + Test.CustomSpanException( + (ReadOnlySpan) characters, + (value, factory) => value.MustBeHexadecimal(factory) + ); + Test.CustomMemoryException(characters.AsMemory(), (value, factory) => value.MustBeHexadecimal(factory)); + Test.CustomMemoryException( + (ReadOnlyMemory) characters, + (value, factory) => value.MustBeHexadecimal(factory) + ); + break; + case GuardFamily.NoWhiteSpace: + Test.CustomSpanException( + characters.AsSpan(), + (value, factory) => value.MustNotContainWhiteSpace(factory) + ); + Test.CustomSpanException( + (ReadOnlySpan) characters, + (value, factory) => value.MustNotContainWhiteSpace(factory) + ); + Test.CustomMemoryException( + characters.AsMemory(), + (value, factory) => value.MustNotContainWhiteSpace(factory) + ); + Test.CustomMemoryException( + (ReadOnlyMemory) characters, + (value, factory) => value.MustNotContainWhiteSpace(factory) + ); + break; + default: throw new ArgumentOutOfRangeException(nameof(family), family, null); + } + } + + private static void AssertStringFailure(string invalidValue, Action guard) => + guard.Invoking(assertion => assertion(invalidValue)) + .Should().ThrowExactly(); + + private enum GuardFamily + { + Digits, + LettersOrDigits, + UpperCase, + LowerCase, + Base64, + Hexadecimal, + NoWhiteSpace, + } +} diff --git a/tests/Light.GuardClauses.Tests/StringAssertions/StringInspectionPredicateTests.cs b/tests/Light.GuardClauses.Tests/StringAssertions/StringInspectionPredicateTests.cs new file mode 100644 index 00000000..4f82f9b8 --- /dev/null +++ b/tests/Light.GuardClauses.Tests/StringAssertions/StringInspectionPredicateTests.cs @@ -0,0 +1,252 @@ +using System; +using System.Buffers.Text; +using System.Reflection; +using FluentAssertions; +using Light.GuardClauses.Exceptions; +using Xunit; +using Xunit.Sdk; + +namespace Light.GuardClauses.Tests.StringAssertions; + +public static class StringInspectionPredicateTests +{ + private static readonly Base64Validator PortableBase64Validator = + typeof(Check).GetMethod("IsBase64Portable", BindingFlags.NonPublic | BindingFlags.Static)! + .CreateDelegate(); + + [Fact] + public static void NullAndEmptyInputsHaveTheDefinedSemantics() + { + string nullText = null; + + nullText.ContainsOnlyDigits().Should().BeFalse(); + nullText.ContainsOnlyLettersOrDigits().Should().BeFalse(); + nullText.IsUpperCase().Should().BeFalse(); + nullText.IsLowerCase().Should().BeFalse(); + nullText.IsBase64().Should().BeFalse(); + nullText.IsHexadecimal().Should().BeFalse(); + + string.Empty.ContainsOnlyDigits().Should().BeTrue(); + string.Empty.ContainsOnlyLettersOrDigits().Should().BeTrue(); + string.Empty.IsUpperCase().Should().BeTrue(); + string.Empty.IsLowerCase().Should().BeTrue(); + string.Empty.IsBase64().Should().BeTrue(); + string.Empty.IsHexadecimal().Should().BeTrue(); + + Span.Empty.ContainsOnlyDigits().Should().BeTrue(); + ReadOnlySpan.Empty.ContainsOnlyLettersOrDigits().Should().BeTrue(); + Memory.Empty.IsUpperCase().Should().BeTrue(); + ReadOnlyMemory.Empty.IsLowerCase().Should().BeTrue(); + Span.Empty.IsBase64().Should().BeTrue(); + ReadOnlyMemory.Empty.IsHexadecimal().Should().BeTrue(); + } + + [Fact] + public static void EveryPredicateSupportsEveryReceiverShape() + { + var digits = "1٢3".ToCharArray(); + var lettersOrDigits = "AzΩ٢".ToCharArray(); + var upperCase = "AΩ 123!".ToCharArray(); + var lowerCase = "aω 123!".ToCharArray(); + var base64 = "T W\tF\ru\n".ToCharArray(); + var hexadecimal = "09aAfF".ToCharArray(); + + digits.AsSpan().ContainsOnlyDigits().Should().BeTrue(); + ((ReadOnlySpan) digits).ContainsOnlyDigits().Should().BeTrue(); + digits.AsMemory().ContainsOnlyDigits().Should().BeTrue(); + ((ReadOnlyMemory) digits).ContainsOnlyDigits().Should().BeTrue(); + + lettersOrDigits.AsSpan().ContainsOnlyLettersOrDigits().Should().BeTrue(); + ((ReadOnlySpan) lettersOrDigits).ContainsOnlyLettersOrDigits().Should().BeTrue(); + lettersOrDigits.AsMemory().ContainsOnlyLettersOrDigits().Should().BeTrue(); + ((ReadOnlyMemory) lettersOrDigits).ContainsOnlyLettersOrDigits().Should().BeTrue(); + + upperCase.AsSpan().IsUpperCase().Should().BeTrue(); + ((ReadOnlySpan) upperCase).IsUpperCase().Should().BeTrue(); + upperCase.AsMemory().IsUpperCase().Should().BeTrue(); + ((ReadOnlyMemory) upperCase).IsUpperCase().Should().BeTrue(); + + lowerCase.AsSpan().IsLowerCase().Should().BeTrue(); + ((ReadOnlySpan) lowerCase).IsLowerCase().Should().BeTrue(); + lowerCase.AsMemory().IsLowerCase().Should().BeTrue(); + ((ReadOnlyMemory) lowerCase).IsLowerCase().Should().BeTrue(); + + base64.AsSpan().IsBase64().Should().BeTrue(); + ((ReadOnlySpan) base64).IsBase64().Should().BeTrue(); + base64.AsMemory().IsBase64().Should().BeTrue(); + ((ReadOnlyMemory) base64).IsBase64().Should().BeTrue(); + + hexadecimal.AsSpan().IsHexadecimal().Should().BeTrue(); + ((ReadOnlySpan) hexadecimal).IsHexadecimal().Should().BeTrue(); + hexadecimal.AsMemory().IsHexadecimal().Should().BeTrue(); + ((ReadOnlyMemory) hexadecimal).IsHexadecimal().Should().BeTrue(); + } + + [Theory] + [InlineData("x12", "1x2", "12x")] + public static void InvalidContentIsDetectedAtTheBeginningMiddleAndEnd(string beginning, string middle, string end) + { + beginning.ContainsOnlyDigits().Should().BeFalse(); + middle.ContainsOnlyDigits().Should().BeFalse(); + end.ContainsOnlyDigits().Should().BeFalse(); + + "-Az".ContainsOnlyLettersOrDigits().Should().BeFalse(); + "A-z".ContainsOnlyLettersOrDigits().Should().BeFalse(); + "Az-".ContainsOnlyLettersOrDigits().Should().BeFalse(); + + "aAB".IsUpperCase().Should().BeFalse(); + "AaB".IsUpperCase().Should().BeFalse(); + "ABa".IsUpperCase().Should().BeFalse(); + + "Aab".IsLowerCase().Should().BeFalse(); + "aAb".IsLowerCase().Should().BeFalse(); + "abA".IsLowerCase().Should().BeFalse(); + + "x09A".IsHexadecimal().Should().BeFalse(); + "0x9A".IsHexadecimal().Should().BeFalse(); + "09Ax".IsHexadecimal().Should().BeFalse(); + } + + [Fact] + public static void CasingAllowsUncasedContentAndUsesUnicodeClassification() + { + "123 !\t中".IsUpperCase().Should().BeTrue(); + "123 !\t中".IsLowerCase().Should().BeTrue(); + "ÄΩ".IsUpperCase().Should().BeTrue(); + "äω".IsLowerCase().Should().BeTrue(); + "Äω".IsUpperCase().Should().BeFalse(); + "äΩ".IsLowerCase().Should().BeFalse(); + } + + [Theory] + [InlineData("", true)] + [InlineData(" \t\r\n", true)] + [InlineData("TWFu", true)] + [InlineData("TWE=", true)] + [InlineData("TQ==", true)] + [InlineData(" T W F u ", true)] + [InlineData("T\tW\rE\n=", true)] + [InlineData("A", false)] + [InlineData("AAA", false)] + [InlineData("AAAAA", false)] + [InlineData("=AAA", false)] + [InlineData("A=AA", false)] + [InlineData("AA=A", false)] + [InlineData("A===", false)] + [InlineData("AA==AA==", false)] + [InlineData("!AAA", false)] + [InlineData("A!AA", false)] + [InlineData("AAA!", false)] + [InlineData("SGVsbG8_", false)] + [InlineData("SGVsbG8-", false)] + [InlineData("AA\v==", false)] + [InlineData("AA\u00A0==", false)] + public static void Base64AcceptsOnlyTheDefinedAlphabetStructureAndWhiteSpace(string value, bool expected) => + value.IsBase64().Should().Be(expected); + + [Fact] + public static void PortableBase64ValidatorMatchesNet10OverDeterministicCorpus() + { + var random = new Random(153); + const string corpusCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=_- \t\r\n\v\u00A0!"; + + for (var iteration = 0; iteration < 10_000; ++iteration) + { + var characters = new char[random.Next(0, 65)]; + for (var index = 0; index < characters.Length; ++index) + { + characters[index] = corpusCharacters[random.Next(corpusCharacters.Length)]; + } + + AssertPortableBase64MatchesFramework(characters); + } + + for (var length = 0; length <= 128; ++length) + { + var bytes = new byte[length]; + random.NextBytes(bytes); + var valid = Convert.ToBase64String(bytes); + AssertPortableBase64MatchesFramework(valid); + AssertPortableBase64MatchesFramework(InsertWhiteSpace(valid)); + } + } + + [Fact] + public static void EveryUtf16CodeUnitMatchesTheDefiningClassifiers() + { + Span value = stackalloc char[1]; + + for (var codeUnit = (int) char.MinValue; codeUnit <= char.MaxValue; ++codeUnit) + { + var character = (char) codeUnit; + value[0] = character; + AssertEqual(char.IsDigit(character), ((ReadOnlySpan) value).ContainsOnlyDigits(), character, "digit"); + AssertEqual(char.IsLetterOrDigit(character), ((ReadOnlySpan) value).ContainsOnlyLettersOrDigits(), character, "letter or digit"); + AssertEqual(!char.IsLower(character), ((ReadOnlySpan) value).IsUpperCase(), character, "upper case"); + AssertEqual(!char.IsUpper(character), ((ReadOnlySpan) value).IsLowerCase(), character, "lower case"); + AssertEqual(IsAsciiHexDigit(character), ((ReadOnlySpan) value).IsHexadecimal(), character, "hexadecimal"); + AssertEqual(!char.IsWhiteSpace(character), IsAcceptedByWhiteSpaceGuard(value), character, "white space"); + } + } + + private static void AssertPortableBase64MatchesFramework(ReadOnlySpan value) + { + var expected = Base64.IsValid(value); + var actual = PortableBase64Validator(value); + if (actual != expected) + { + throw new XunitException($"Portable Base64 mismatch for '{value.ToString()}': expected {expected}, actual {actual}."); + } + } + + private static string InsertWhiteSpace(string value) + { + if (value.Length == 0) + { + return " \t\r\n"; + } + + var result = new char[value.Length * 2]; + for (var index = 0; index < value.Length; ++index) + { + result[index * 2] = value[index]; + result[index * 2 + 1] = (char) (index % 4 switch + { + 0 => ' ', + 1 => '\t', + 2 => '\r', + _ => '\n', + }); + } + + return new (result); + } + + private static bool IsAcceptedByWhiteSpaceGuard(ReadOnlySpan value) + { + try + { + value.MustNotContainWhiteSpace(); + return true; + } + catch (StringException) + { + return false; + } + } + + private static bool IsAsciiHexDigit(char value) => + value is >= '0' and <= '9' or >= 'A' and <= 'F' or >= 'a' and <= 'f'; + + private static void AssertEqual(bool expected, bool actual, char value, string classification) + { + if (actual != expected) + { + throw new XunitException( + $"UTF-16 code unit U+{(int) value:X4} had an unexpected {classification} classification: expected {expected}, actual {actual}." + ); + } + } + + private delegate bool Base64Validator(ReadOnlySpan value); +} diff --git a/tests/Light.GuardClauses.Tests/Test.cs b/tests/Light.GuardClauses.Tests/Test.cs index d2956eab..eadb215f 100644 --- a/tests/Light.GuardClauses.Tests/Test.cs +++ b/tests/Light.GuardClauses.Tests/Test.cs @@ -4,7 +4,7 @@ using FluentAssertions; using FluentAssertions.Specialized; using Light.GuardClauses.ExceptionFactory; -using Xunit.Abstractions; +using Xunit; using Xunit.Sdk; namespace Light.GuardClauses.Tests; diff --git a/tools/source-export/Light.GuardClauses.SourceCodeTransformation/AssertionWhitelist.cs b/tools/source-export/Light.GuardClauses.SourceCodeTransformation/AssertionWhitelist.cs index eebdb94d..91d9fabf 100644 --- a/tools/source-export/Light.GuardClauses.SourceCodeTransformation/AssertionWhitelist.cs +++ b/tools/source-export/Light.GuardClauses.SourceCodeTransformation/AssertionWhitelist.cs @@ -9,6 +9,10 @@ public sealed class AssertionWhitelist { public bool IsEnabled { get; init; } = false; + public AssertionEntry ContainsOnlyDigits { get; init; } = new(); + + public AssertionEntry ContainsOnlyLettersOrDigits { get; init; } = new(); + public AssertionEntry DerivesFrom { get; init; } = new(); public new AssertionEntry Equals { get; init; } = new(); @@ -25,6 +29,8 @@ public sealed class AssertionWhitelist public AssertionEntry IsAscii { get; init; } = new(); + public AssertionEntry IsBase64 { get; init; } = new(); + public AssertionEntry IsApproximately { get; init; } = new(); public AssertionEntry IsDigit { get; init; } = new(); @@ -43,6 +49,8 @@ public sealed class AssertionWhitelist public AssertionEntry IsGreaterThanOrApproximately { get; init; } = new(); + public AssertionEntry IsHexadecimal { get; init; } = new(); + public AssertionEntry IsIn { get; init; } = new(); public AssertionEntry IsLessThanOrApproximately { get; init; } = new(); @@ -51,6 +59,8 @@ public sealed class AssertionWhitelist public AssertionEntry IsLetterOrDigit { get; init; } = new(); + public AssertionEntry IsLowerCase { get; init; } = new(); + public AssertionEntry IsNewLine { get; init; } = new(); public AssertionEntry IsNotIn { get; init; } = new(); @@ -79,6 +89,8 @@ public sealed class AssertionWhitelist public AssertionEntry IsTrimmedAtStart { get; init; } = new(); + public AssertionEntry IsUpperCase { get; init; } = new(); + public AssertionEntry IsUuidVersion7 { get; init; } = new(); public AssertionEntry IsValidEnumValue { get; init; } = new(); @@ -89,6 +101,8 @@ public sealed class AssertionWhitelist public AssertionEntry MustBe { get; init; } = new(); + public AssertionEntry MustBeBase64 { get; init; } = new(); + public AssertionEntry MustBeAbsoluteUri { get; init; } = new(); public AssertionEntry MustBeApproximately { get; init; } = new(); @@ -105,6 +119,8 @@ public sealed class AssertionWhitelist public AssertionEntry MustBeGreaterThanOrEqualTo { get; init; } = new(); + public AssertionEntry MustBeHexadecimal { get; init; } = new(); + public AssertionEntry MustBeHttpOrHttpsUrl { get; init; } = new(); public AssertionEntry MustBeHttpUrl { get; init; } = new(); @@ -125,6 +141,8 @@ public sealed class AssertionWhitelist public AssertionEntry MustBeLongerThanOrEqualTo { get; init; } = new(); + public AssertionEntry MustBeLowerCase { get; init; } = new(); + public AssertionEntry MustBeNegative { get; init; } = new(); public AssertionEntry MustBeNewLine { get; init; } = new(); @@ -151,6 +169,8 @@ public sealed class AssertionWhitelist public AssertionEntry MustBeUnspecified { get; init; } = new(); + public AssertionEntry MustBeUpperCase { get; init; } = new(); + public AssertionEntry MustBeUtc { get; init; } = new(); public AssertionEntry MustBeUuidVersion7 { get; init; } = new(); @@ -159,6 +179,10 @@ public sealed class AssertionWhitelist public AssertionEntry MustContain { get; init; } = new(); + public AssertionEntry MustContainOnlyDigits { get; init; } = new(); + + public AssertionEntry MustContainOnlyLettersOrDigits { get; init; } = new(); + public AssertionEntry MustContainKey { get; init; } = new(); public AssertionEntry MustEndWith { get; init; } = new(); @@ -237,6 +261,8 @@ public sealed class AssertionWhitelist public AssertionEntry MustNotContainNullOrWhiteSpace { get; init; } = new(); + public AssertionEntry MustNotContainWhiteSpace { get; init; } = new(); + public AssertionEntry MustNotEndWith { get; init; } = new(); public AssertionEntry MustNotStartWith { get; init; } = new(); diff --git a/tools/source-export/Light.GuardClauses.SourceCodeTransformation/SourceFileMerger.cs b/tools/source-export/Light.GuardClauses.SourceCodeTransformation/SourceFileMerger.cs index 633b965f..cc94afd4 100644 --- a/tools/source-export/Light.GuardClauses.SourceCodeTransformation/SourceFileMerger.cs +++ b/tools/source-export/Light.GuardClauses.SourceCodeTransformation/SourceFileMerger.cs @@ -72,7 +72,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE using System.Runtime.Serialization; using System.Text; using System.Text.RegularExpressions; -{(options.TargetFramework == SourceTargetFramework.Net10_0 ? "using System.Numerics;" + Environment.NewLine : string.Empty)}{(options.IncludeJetBrainsAnnotationsUsing ? "using JetBrains.Annotations;" + Environment.NewLine : string.Empty)}using {options.BaseNamespace}.Exceptions; +{(options.TargetFramework == SourceTargetFramework.Net10_0 ? "using System.Buffers.Text;" + Environment.NewLine + "using System.Numerics;" + Environment.NewLine : string.Empty)}{(options.IncludeJetBrainsAnnotationsUsing ? "using JetBrains.Annotations;" + Environment.NewLine : string.Empty)}using {options.BaseNamespace}.Exceptions; using {options.BaseNamespace}.ExceptionFactory; using {options.BaseNamespace}.FrameworkExtensions; {(options.IncludeJetBrainsAnnotationsUsing ? "using NotNullAttribute = System.Diagnostics.CodeAnalysis.NotNullAttribute;" : "")} diff --git a/tools/source-export/Light.GuardClauses.SourceCodeTransformation/settings.json b/tools/source-export/Light.GuardClauses.SourceCodeTransformation/settings.json index 41b18a6b..72ee7e54 100644 --- a/tools/source-export/Light.GuardClauses.SourceCodeTransformation/settings.json +++ b/tools/source-export/Light.GuardClauses.SourceCodeTransformation/settings.json @@ -18,6 +18,8 @@ "RemoveCallerArgumentExpressions": false, "AssertionWhitelist": { "IsEnabled": false, + "ContainsOnlyDigits": { "Include": true, "IncludeExceptionFactoryOverload": true }, + "ContainsOnlyLettersOrDigits": { "Include": true, "IncludeExceptionFactoryOverload": true }, "DerivesFrom": { "Include": true, "IncludeExceptionFactoryOverload": true }, "Equals": { "Include": true, "IncludeExceptionFactoryOverload": true }, "Implements": { "Include": true, "IncludeExceptionFactoryOverload": true }, @@ -26,6 +28,7 @@ "InvalidOperation": { "Include": true, "IncludeExceptionFactoryOverload": true }, "InvalidState": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsAscii": { "Include": true, "IncludeExceptionFactoryOverload": true }, + "IsBase64": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsApproximately": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsDigit": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsEmailAddress": { "Include": true, "IncludeExceptionFactoryOverload": true }, @@ -35,10 +38,12 @@ "IsFileExtension": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsFinite": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsGreaterThanOrApproximately": { "Include": true, "IncludeExceptionFactoryOverload": true }, + "IsHexadecimal": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsIn": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsLessThanOrApproximately": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsLetter": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsLetterOrDigit": { "Include": true, "IncludeExceptionFactoryOverload": true }, + "IsLowerCase": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsNewLine": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsNotIn": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsNullOrEmpty": { "Include": true, "IncludeExceptionFactoryOverload": true }, @@ -53,10 +58,12 @@ "IsTrimmed": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsTrimmedAtEnd": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsTrimmedAtStart": { "Include": true, "IncludeExceptionFactoryOverload": true }, + "IsUpperCase": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsUuidVersion7": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsValidEnumValue": { "Include": true, "IncludeExceptionFactoryOverload": true }, "IsWhiteSpace": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeAscii": { "Include": true, "IncludeExceptionFactoryOverload": true }, + "MustBeBase64": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBe": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeAbsoluteUri": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeApproximately": { "Include": true, "IncludeExceptionFactoryOverload": true }, @@ -66,6 +73,7 @@ "MustBeGreaterThan": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeGreaterThanOrApproximately": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeGreaterThanOrEqualTo": { "Include": true, "IncludeExceptionFactoryOverload": true }, + "MustBeHexadecimal": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeHttpOrHttpsUrl": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeHttpUrl": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeHttpsUrl": { "Include": true, "IncludeExceptionFactoryOverload": true }, @@ -76,6 +84,7 @@ "MustBeLocal": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeLongerThan": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeLongerThanOrEqualTo": { "Include": true, "IncludeExceptionFactoryOverload": true }, + "MustBeLowerCase": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeNegative": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeNewLine": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeOfType": { "Include": true, "IncludeExceptionFactoryOverload": true }, @@ -89,10 +98,13 @@ "MustBeTrimmedAtEnd": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeTrimmedAtStart": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeUnspecified": { "Include": true, "IncludeExceptionFactoryOverload": true }, + "MustBeUpperCase": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeUtc": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeUuidVersion7": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustBeValidEnumValue": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustContain": { "Include": true, "IncludeExceptionFactoryOverload": true }, + "MustContainOnlyDigits": { "Include": true, "IncludeExceptionFactoryOverload": true }, + "MustContainOnlyLettersOrDigits": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustContainKey": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustEndWith": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustHaveCount": { "Include": true, "IncludeExceptionFactoryOverload": true }, @@ -132,6 +144,7 @@ "MustNotContainKey": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustNotContainNull": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustNotContainNullOrWhiteSpace": { "Include": true, "IncludeExceptionFactoryOverload": true }, + "MustNotContainWhiteSpace": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustNotEndWith": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustNotStartWith": { "Include": true, "IncludeExceptionFactoryOverload": true }, "MustStartWith": { "Include": true, "IncludeExceptionFactoryOverload": true }