diff --git a/Code/Light.GuardClauses.Tests/ExecuteReadOnlySpanAssertion.cs b/Code/Light.GuardClauses.Tests/ExecuteReadOnlySpanAssertion.cs new file mode 100644 index 0000000..32536e1 --- /dev/null +++ b/Code/Light.GuardClauses.Tests/ExecuteReadOnlySpanAssertion.cs @@ -0,0 +1,26 @@ +#nullable enable +using System; + +namespace Light.GuardClauses.Tests; + +public delegate void ExecuteReadOnlySpanAssertion( + ReadOnlySpan span, + ReadOnlySpanExceptionFactory exceptionFactory +); + +public delegate void ExecuteSpanAssertion( + Span span, + ReadOnlySpanExceptionFactory exceptionFactory +); + +public delegate void ExecuteReadOnlySpanAssertion( + ReadOnlySpan span, + TValue additionalValue, + ReadOnlySpanExceptionFactory exceptionFactory +); + +public delegate void ExecuteSpanAssertion( + Span span, + TValue additionalValue, + ReadOnlySpanExceptionFactory exceptionFactory +); diff --git a/Code/Light.GuardClauses.Tests/StringAssertions/InvalidEmailAddresses.cs b/Code/Light.GuardClauses.Tests/StringAssertions/InvalidEmailAddresses.cs new file mode 100644 index 0000000..a31d041 --- /dev/null +++ b/Code/Light.GuardClauses.Tests/StringAssertions/InvalidEmailAddresses.cs @@ -0,0 +1,33 @@ +using Xunit; + +namespace Light.GuardClauses.Tests.StringAssertions; + +public class InvalidEmailAddresses : TheoryData +{ + public InvalidEmailAddresses() + { + Add("plainaddress"); + Add("#@%^%#$@#$@#.com"); + Add("@domain.com"); + Add("Joe Smith "); + Add("email.domain.com"); + Add("email@domain@domain.com"); + Add(".email@domain.com"); + Add("email.@domain.com"); + Add("email..email@domain.com"); + Add("email@domain.com (Joe Smith)"); + Add("email@domain"); + Add("email@-domain.com"); + Add("email@111.222.333.44444"); + Add("email@domain..com"); + Add("email@256.256.256.256"); + } +} + +public sealed class InvalidEmailAddressesWithNull : InvalidEmailAddresses +{ + public InvalidEmailAddressesWithNull() + { + Add(null); + } +} diff --git a/Code/Light.GuardClauses.Tests/StringAssertions/IsEmailAddressTest.cs b/Code/Light.GuardClauses.Tests/StringAssertions/IsEmailAddressTest.cs deleted file mode 100644 index 9e0b994..0000000 --- a/Code/Light.GuardClauses.Tests/StringAssertions/IsEmailAddressTest.cs +++ /dev/null @@ -1,50 +0,0 @@ -using FluentAssertions; -using Xunit; - -namespace Light.GuardClauses.Tests.StringAssertions; - -public sealed class IsEmailAddressTest -{ - [Theory] - [InlineData(null)] - [InlineData("plainaddress")] - [InlineData("#@%^%#$@#$@#.com")] - [InlineData("@domain.com")] - [InlineData("Joe Smith ")] - [InlineData("email.domain.com")] - [InlineData("email@domain@domain.com")] - [InlineData(".email@domain.com")] - [InlineData("email.@domain.com")] - [InlineData("email..email@domain.com")] - [InlineData("あいうえお@domain.com")] - [InlineData("email@domain.com (Joe Smith)")] - [InlineData("email@domain")] - [InlineData("email@-domain.com")] - [InlineData("email@111.222.333.44444")] - [InlineData("email@domain..com")] - public void IsNotValidEmailAddress(string email) - { - var isValid = email.IsEmailAddress(); - - isValid.Should().BeFalse(); - } - - [Theory] - [InlineData("email@domain.com")] - [InlineData("firstname.lastname@domain.com")] - [InlineData("email@subdomain.domain.com")] - [InlineData("firstname+lastname@domain.com")] - [InlineData("email@123.123.123.123")] - [InlineData("1234567890@domain.com")] - [InlineData("email@domain-one.com")] - [InlineData("_______@domain.com")] - [InlineData("email@domain.name")] - [InlineData("email@domain.co.jp")] - [InlineData("firstname-lastname@domain.com")] - public void IsValidEmailAddress(string email) - { - var isValid = email.IsEmailAddress(); - - isValid.Should().BeTrue(); - } -} \ No newline at end of file diff --git a/Code/Light.GuardClauses.Tests/StringAssertions/IsEmailAddressTests.cs b/Code/Light.GuardClauses.Tests/StringAssertions/IsEmailAddressTests.cs new file mode 100644 index 0000000..0cd3750 --- /dev/null +++ b/Code/Light.GuardClauses.Tests/StringAssertions/IsEmailAddressTests.cs @@ -0,0 +1,108 @@ +using System; +using FluentAssertions; +using Xunit; + +namespace Light.GuardClauses.Tests.StringAssertions; + +public sealed class IsEmailAddressTests +{ + [Theory] + [ClassData(typeof(InvalidEmailAddressesWithNull))] + public void IsNotValidEmailAddress(string email) + { + var isValid = email.IsEmailAddress(); + + isValid.Should().BeFalse(); + } + + [Theory] + [ClassData(typeof(ValidEmailAddresses))] + public void IsValidEmailAddress(string email) + { + var isValid = email.IsEmailAddress(); + + isValid.Should().BeTrue(); + } + +#if NET8_0 + [Theory] + [ClassData(typeof(InvalidEmailAddressesWithNull))] + public void IsNotValidEmailAddress_ReadOnlySpan(string email) + { + var span = new ReadOnlySpan(email?.ToCharArray() ?? []); + var isValid = span.IsEmailAddress(); + + isValid.Should().BeFalse(); + } + + [Theory] + [ClassData(typeof(ValidEmailAddresses))] + public void IsValidEmailAddress_ReadOnlySpan(string email) + { + var span = email.AsSpan(); + var isValid = span.IsEmailAddress(); + + isValid.Should().BeTrue(); + } + + [Theory] + [ClassData(typeof(InvalidEmailAddressesWithNull))] + public void IsNotValidEmailAddress_Span(string email) + { + var span = new Span(email?.ToCharArray() ?? []); + var isValid = span.IsEmailAddress(); + + isValid.Should().BeFalse(); + } + + [Theory] + [ClassData(typeof(ValidEmailAddresses))] + public void IsValidEmailAddress_Span(string email) + { + var span = new Span(email.ToCharArray()); + var isValid = span.IsEmailAddress(); + + isValid.Should().BeTrue(); + } + + [Theory] + [ClassData(typeof(InvalidEmailAddressesWithNull))] + public void IsNotValidEmailAddress_Memory(string email) + { + var memory = email?.ToCharArray().AsMemory() ?? Memory.Empty; + var isValid = memory.IsEmailAddress(); + + isValid.Should().BeFalse(); + } + + [Theory] + [ClassData(typeof(ValidEmailAddresses))] + public void IsValidEmailAddress_Memory(string email) + { + var memory = email.ToCharArray().AsMemory(); + var isValid = memory.IsEmailAddress(); + + isValid.Should().BeTrue(); + } + + [Theory] + [ClassData(typeof(InvalidEmailAddressesWithNull))] + public void IsNotValidEmailAddress_ReadOnlyMemory(string email) + { + var memory = new ReadOnlyMemory(email?.ToCharArray() ?? []); + var isValid = memory.IsEmailAddress(); + + isValid.Should().BeFalse(); + } + + [Theory] + [ClassData(typeof(ValidEmailAddresses))] + public void IsValidEmailAddress_ReadOnlyMemory(string email) + { + var memory = new ReadOnlyMemory(email.ToCharArray()); + var isValid = memory.IsEmailAddress(); + + isValid.Should().BeTrue(); + } +#endif +} diff --git a/Code/Light.GuardClauses.Tests/StringAssertions/MustBeEmailAddressTests.cs b/Code/Light.GuardClauses.Tests/StringAssertions/MustBeEmailAddressTests.cs index 8d75d3a..f6d1314 100644 --- a/Code/Light.GuardClauses.Tests/StringAssertions/MustBeEmailAddressTests.cs +++ b/Code/Light.GuardClauses.Tests/StringAssertions/MustBeEmailAddressTests.cs @@ -9,35 +9,36 @@ namespace Light.GuardClauses.Tests.StringAssertions; public static class MustBeEmailAddressTests { [Theory] - [InlineData("email@domain.com")] - [InlineData("email@123.123.123.123")] - public static void ValidEmailAddress(string emailAddress) => emailAddress.MustBeEmailAddress().Should().BeSameAs(emailAddress); + [ClassData(typeof(ValidEmailAddresses))] + public static void ValidEmailAddress(string emailAddress) => + emailAddress.MustBeEmailAddress().Should().BeSameAs(emailAddress); [Theory] - [InlineData("plainAddress")] - [InlineData("Joe Smith ")] + [ClassData(typeof(InvalidEmailAddresses))] public static void InvalidEmailAddress(string emailAddress) { Action act = () => emailAddress.MustBeEmailAddress(); act.Should().Throw() - .And.Message.Should().Contain($"emailAddress must be a valid email address, but it actually is \"{emailAddress}\"."); + .And.Message.Should().Contain( + $"emailAddress must be a valid email address, but it actually is \"{emailAddress}\"." + ); } [Theory] - [InlineData(".email@domain.com")] - [InlineData("email.@domain.com")] + [ClassData(typeof(InvalidEmailAddresses))] public static void InvalidEmailAddressArgumentName(string emailAddress) { Action act = () => emailAddress.MustBeEmailAddress(nameof(emailAddress)); act.Should().Throw() - .And.Message.Should().Contain($"emailAddress must be a valid email address, but it actually is \"{emailAddress}\"."); + .And.Message.Should().Contain( + $"emailAddress must be a valid email address, but it actually is \"{emailAddress}\"." + ); } [Theory] - [InlineData("email@domain")] - [InlineData("email@-domain.com")] + [ClassData(typeof(InvalidEmailAddresses))] public static void InvalidEmailAddressCustomMessage(string emailAddress) { const string customMessage = "This email address is not valid"; @@ -55,7 +56,7 @@ public static void InvalidEmailCustomRegex() { const string email = "email@domain@domain.com"; - Action act = () => email.MustBeEmailAddress(CustomRegex, "email"); + Action act = () => email.MustBeEmailAddress(CustomRegex); act.Should().Throw() .And.Message.Should().Contain($"email must be a valid email address, but it actually is \"{email}\"."); @@ -82,27 +83,35 @@ public static void NullCustomRegex(string email, Regex regex) new () { { null, CustomRegex }, - { "invalidEmailAddress", null } + { "invalidEmailAddress", null }, }; [Fact] public static void CustomException() => - Test.CustomException("email.domain.com", - (input, exceptionFactory) => input.MustBeEmailAddress(exceptionFactory)); + Test.CustomException( + "email.domain.com", + (input, exceptionFactory) => input.MustBeEmailAddress(exceptionFactory) + ); [Fact] public static void CustomMessage() => - Test.CustomMessage(message => "#@%^%#$@#$@#.com".MustBeEmailAddress(message: message)); + Test.CustomMessage( + message => "#@%^%#$@#$@#.com".MustBeEmailAddress(message: message) + ); [Fact] public static void CustomExceptionCustomRegex() => - Test.CustomException("invalidEmailAddress", - CustomRegex, - (i, r, exceptionFactory) => i.MustBeEmailAddress(r, exceptionFactory)); + Test.CustomException( + "invalidEmailAddress", + CustomRegex, + (i, r, exceptionFactory) => i.MustBeEmailAddress(r, exceptionFactory) + ); [Fact] public static void CustomMessageCustomRegex() => - Test.CustomMessage(message => "invalidEmailAddress".MustBeEmailAddress(CustomRegex, message: message)); + Test.CustomMessage( + message => "invalidEmailAddress".MustBeEmailAddress(CustomRegex, message: message) + ); [Fact] public static void CallerArgumentExpression() @@ -114,4 +123,346 @@ public static void CallerArgumentExpression() act.Should().Throw() .WithParameterName(nameof(email)); } -} \ No newline at end of file + +#if NET8_0 + [Theory] + [ClassData(typeof(ValidEmailAddresses))] + public static void ValidEmailAddress_ReadOnlySpan(string email) + { + var result = email.AsSpan().MustBeEmailAddress(); + result.ToString().Should().Be(email); + } + + [Theory] + [ClassData(typeof(InvalidEmailAddresses))] + public static void InvalidEmailAddress_ReadOnlySpan(string email) + { + var act = () => + { + var readOnlySpan = email.AsSpan(); + readOnlySpan.MustBeEmailAddress(); + }; + act.Should().Throw() + .And.Message.Should() + .Contain($"readOnlySpan must be a valid email address, but it actually is \"{email}\"."); + } + + [Theory] + [ClassData(typeof(InvalidEmailAddresses))] + public static void InvalidEmailAddressArgumentName_ReadOnlySpan(string email) + { + Action act = () => email.AsSpan().MustBeEmailAddress(nameof(email)); + act.Should().Throw() + .And.Message.Should().Contain(nameof(email)); + } + + [Theory] + [ClassData(typeof(InvalidEmailAddresses))] + public static void InvalidEmailAddressCustomMessage_ReadOnlySpan(string email) + { + const string customMessage = "This email address is not valid"; + Action act = () => email.AsSpan().MustBeEmailAddress(nameof(email), customMessage); + act.Should().Throw() + .And.Message.Should().Contain(customMessage); + } + + [Fact] + public static void CallerArgumentExpression_ReadOnlySpan() + { + var act = () => + { + var email = "This is not an email address".AsSpan(); + email.MustBeEmailAddress(); + }; + act.Should().Throw() + .WithParameterName("email"); + } + + [Fact] + public static void CustomException_ReadOnlySpan() => + Test.CustomSpanException( + "email.domain.com".AsSpan(), + (input, exceptionFactory) => input.MustBeEmailAddress(exceptionFactory) + ); + + [Fact] + public static void CustomExceptionCustomRegex_ReadOnlySpan() => + Test.CustomSpanException( + "invalidEmailAddress".AsSpan(), + CustomRegex, + (i, r, exceptionFactory) => i.MustBeEmailAddress(r, exceptionFactory) + ); + + [Fact] + public static void CustomMessageCustomRegex_ReadOnlySpan() => + Test.CustomMessage( + message => "invalidEmailAddress".AsSpan().MustBeEmailAddress(CustomRegex, message: message) + ); + + // Tests for Span + [Theory] + [ClassData(typeof(ValidEmailAddresses))] + public static void ValidEmailAddress_Span(string email) + { + var emailChars = email.ToCharArray(); + var span = new Span(emailChars); + var result = span.MustBeEmailAddress(); + new string(result).Should().Be(email); + } + + [Theory] + [ClassData(typeof(InvalidEmailAddresses))] + public static void InvalidEmailAddress_Span(string email) + { + var emailChars = email.ToCharArray(); + var act = () => + { + var span = new Span(emailChars); + span.MustBeEmailAddress(); + }; + act.Should().Throw() + .And.Message.Should().Contain($"span must be a valid email address, but it actually is \"{email}\"."); + } + + [Theory] + [ClassData(typeof(InvalidEmailAddresses))] + public static void InvalidEmailAddressArgumentName_Span(string email) + { + var emailChars = email.ToCharArray(); + var act = () => + { + var span = new Span(emailChars); + span.MustBeEmailAddress(nameof(span)); + }; + act.Should().Throw() + .And.Message.Should().Contain("span"); + } + + [Theory] + [ClassData(typeof(InvalidEmailAddresses))] + public static void InvalidEmailAddressCustomMessage_Span(string email) + { + const string customMessage = "This email address is not valid"; + var emailChars = email.ToCharArray(); + var act = () => + { + var span = new Span(emailChars); + span.MustBeEmailAddress(nameof(span), customMessage); + }; + act.Should().Throw() + .And.Message.Should().Contain(customMessage); + } + + [Fact] + public static void CallerArgumentExpression_Span() + { + var act = () => + { + var emailChars = "This is not an email address".ToCharArray(); + var span = new Span(emailChars); + span.MustBeEmailAddress(); + }; + act.Should().Throw() + .WithParameterName("span"); + } + + [Fact] + public static void CustomException_Span() + { + var emailChars = "email.domain.com".ToCharArray(); + var span = new Span(emailChars); + Test.CustomSpanException( + span, + (input, exceptionFactory) => input.MustBeEmailAddress(exceptionFactory) + ); + } + + [Fact] + public static void CustomExceptionCustomRegex_Span() + { + var emailChars = "invalidEmailAddress".ToCharArray(); + var span = new Span(emailChars); + Test.CustomSpanException( + span, + CustomRegex, + (i, r, exceptionFactory) => i.MustBeEmailAddress(r, exceptionFactory) + ); + } + + [Fact] + public static void CustomMessageCustomRegex_Span() => + Test.CustomMessage( + message => + { + var span = new Span("invalidEmailAddress".ToCharArray()); + span.MustBeEmailAddress(CustomRegex, message: message); + } + ); + + // Tests for Memory + [Theory] + [ClassData(typeof(ValidEmailAddresses))] + public static void ValidEmailAddress_Memory(string email) + { + var memory = email.ToCharArray().AsMemory(); + var result = memory.MustBeEmailAddress(); + result.ToString().Should().Be(email); + } + + [Theory] + [ClassData(typeof(InvalidEmailAddresses))] + public static void InvalidEmailAddress_Memory(string email) + { + var memory = email.ToCharArray().AsMemory(); + Action act = () => memory.MustBeEmailAddress(); + act.Should().Throw() + .And.Message.Should().Contain($"memory must be a valid email address, but it actually is \"{email}\"."); + } + + [Theory] + [ClassData(typeof(InvalidEmailAddresses))] + public static void InvalidEmailAddressArgumentName_Memory(string email) + { + var memory = email.ToCharArray().AsMemory(); + Action act = () => memory.MustBeEmailAddress(nameof(memory)); + act.Should().Throw() + .And.Message.Should().Contain(nameof(memory)); + } + + [Theory] + [ClassData(typeof(InvalidEmailAddresses))] + public static void InvalidEmailAddressCustomMessage_Memory(string email) + { + const string customMessage = "This email address is not valid"; + var memory = email.ToCharArray().AsMemory(); + Action act = () => memory.MustBeEmailAddress(nameof(memory), customMessage); + act.Should().Throw() + .And.Message.Should().Contain(customMessage); + } + + [Fact] + public static void CallerArgumentExpression_Memory() + { + var act = () => + { + var memory = "This is not an email address".ToCharArray().AsMemory(); + memory.MustBeEmailAddress(); + }; + act.Should().Throw() + .WithParameterName("memory"); + } + + [Fact] + public static void CustomException_Memory() + { + var memory = "email.domain.com".ToCharArray().AsMemory(); + Test.CustomMemoryException( + memory, + (input, exceptionFactory) => input.MustBeEmailAddress(exceptionFactory) + ); + } + + [Fact] + public static void CustomExceptionCustomRegex_Memory() + { + var memory = "invalidEmailAddress".ToCharArray().AsMemory(); + Test.CustomMemoryException( + memory, + CustomRegex, + (i, r, exceptionFactory) => i.MustBeEmailAddress(r, exceptionFactory) + ); + } + + [Fact] + public static void CustomMessageCustomRegex_Memory() + { + var memory = "invalidEmailAddress".AsMemory(); + Test.CustomMessage( + message => memory.MustBeEmailAddress(CustomRegex, message: message) + ); + } + + // Tests for ReadOnlyMemory + [Theory] + [ClassData(typeof(ValidEmailAddresses))] + public static void ValidEmailAddress_ReadOnlyMemory(string email) + { + var memory = email.AsMemory(); + var result = memory.MustBeEmailAddress(); + result.ToString().Should().Be(email); + } + + [Theory] + [ClassData(typeof(InvalidEmailAddresses))] + public static void InvalidEmailAddress_ReadOnlyMemory(string email) + { + var memory = email.AsMemory(); + Action act = () => memory.MustBeEmailAddress(); + act.Should().Throw() + .And.Message.Should().StartWith($"memory must be a valid email address, but it actually is \"{email}\"."); + } + + [Theory] + [ClassData(typeof(InvalidEmailAddresses))] + public static void InvalidEmailAddressArgumentName_ReadOnlyMemory(string email) + { + var readOnlyMemory = email.AsMemory(); + Action act = () => readOnlyMemory.MustBeEmailAddress(nameof(readOnlyMemory)); + act.Should().Throw() + .And.Message.Should().Contain(nameof(readOnlyMemory)); + } + + [Theory] + [ClassData(typeof(InvalidEmailAddresses))] + public static void InvalidEmailAddressCustomMessage_ReadOnlyMemory(string email) + { + const string customMessage = "This email address is not valid"; + var readOnlyMemory = email.AsMemory(); + Action act = () => readOnlyMemory.MustBeEmailAddress(nameof(readOnlyMemory), customMessage); + act.Should().Throw() + .And.Message.Should().Contain(customMessage); + } + + [Fact] + public static void CallerArgumentExpression_ReadOnlyMemory() + { + var act = () => + { + var readOnlyMemory = "This is not an email address".AsMemory(); + readOnlyMemory.MustBeEmailAddress(); + }; + act.Should().Throw() + .WithParameterName("readOnlyMemory"); + } + + [Fact] + public static void CustomException_ReadOnlyMemory() + { + var readOnlyMemory = "email.domain.com".AsMemory(); + Test.CustomMemoryException( + readOnlyMemory, + (input, exceptionFactory) => input.MustBeEmailAddress(exceptionFactory) + ); + } + + [Fact] + public static void CustomExceptionCustomRegex_ReadOnlyMemory() + { + var readOnlyMemory = "invalidEmailAddress".AsMemory(); + Test.CustomMemoryException( + readOnlyMemory, + CustomRegex, + (i, r, exceptionFactory) => i.MustBeEmailAddress(r, exceptionFactory) + ); + } + + [Fact] + public static void CustomMessageCustomRegex_ReadOnlyMemory() + { + var readOnlyMemory = "invalidEmailAddress".AsMemory(); + Test.CustomMessage( + message => readOnlyMemory.MustBeEmailAddress(CustomRegex, message: message) + ); + } +#endif +} diff --git a/Code/Light.GuardClauses.Tests/StringAssertions/ValidEmailAddresses.cs b/Code/Light.GuardClauses.Tests/StringAssertions/ValidEmailAddresses.cs new file mode 100644 index 0000000..d134ce1 --- /dev/null +++ b/Code/Light.GuardClauses.Tests/StringAssertions/ValidEmailAddresses.cs @@ -0,0 +1,28 @@ +using Xunit; + +namespace Light.GuardClauses.Tests.StringAssertions; + +public sealed class ValidEmailAddresses : TheoryData +{ + public ValidEmailAddresses() + { + Add("email@domain.com"); + Add("firstname.lastname@domain.com"); + Add("email@subdomain.domain.com"); + Add("firstname+lastname@domain.com"); + Add("email@123.123.123.123"); + Add("1234567890@domain.com"); + Add("email@domain-one.com"); + Add("_______@domain.com"); + Add("email@domain.name"); + Add("email@domain.co.jp"); + Add("firstname-lastname@domain.com"); + Add("email@domain.museum"); // Long TLD (>4 chars) + Add("email@domain.travel"); // Another long TLD + Add("email@domain.photography"); // Even longer TLD + Add("email@[IPv6:2001:db8::1]"); // IPv6 format + Add("\"quoted\"@domain.com"); // Quoted local part + Add("user.name+tag+sorting@example.com"); // Gmail-style + addressing + Add("あいうえお@domain.com"); // Unicode character test + } +} diff --git a/Code/Light.GuardClauses.Tests/Test.cs b/Code/Light.GuardClauses.Tests/Test.cs index 9487c42..42922f5 100644 --- a/Code/Light.GuardClauses.Tests/Test.cs +++ b/Code/Light.GuardClauses.Tests/Test.cs @@ -1,11 +1,11 @@ -using System; +#nullable enable + +using System; using FluentAssertions; using FluentAssertions.Specialized; using Xunit.Abstractions; using Xunit.Sdk; -#nullable enable - namespace Light.GuardClauses.Tests; public static class Test @@ -48,7 +48,108 @@ Exception ExceptionFactory(T parameter) } } - public static void CustomException(T1 first, T2 second, Action> executeAssertion) + public static void CustomSpanException( + ReadOnlySpan invalidValue, + ExecuteReadOnlySpanAssertion executeAssertion + ) + { + T[]? capturedParameter = null; + + Exception ExceptionFactory(ReadOnlySpan parameter) + { + capturedParameter = parameter.ToArray(); + return Exception; + } + + try + { + executeAssertion(invalidValue, ExceptionFactory); + throw new XunitException("The assertion should have thrown a custom exception at this point."); + } + catch (ExceptionDummy exception) + { + exception.Should().BeSameAs(exception); + capturedParameter.Should().Equal(invalidValue.ToArray()); + } + } + + public static void CustomSpanException(Span invalidValue, ExecuteSpanAssertion executeAssertion) + { + T[]? capturedParameter = null; + + Exception ExceptionFactory(ReadOnlySpan parameter) + { + capturedParameter = parameter.ToArray(); + return Exception; + } + + try + { + executeAssertion(invalidValue, ExceptionFactory); + throw new XunitException("The assertion should have thrown a custom exception at this point."); + } + catch (ExceptionDummy exception) + { + exception.Should().BeSameAs(exception); + capturedParameter.Should().Equal(invalidValue.ToArray()); + } + } + + public static void CustomMemoryException( + Memory invalidValue, + Action, ReadOnlySpanExceptionFactory> executeAssertion + ) + { + Memory capturedParameter = default; + + Exception ExceptionFactory(ReadOnlySpan parameter) + { + capturedParameter = parameter.ToArray(); + return Exception; + } + + try + { + executeAssertion(invalidValue, ExceptionFactory); + throw new XunitException("The assertion should have thrown a custom exception at this point."); + } + catch (ExceptionDummy exception) + { + exception.Should().BeSameAs(exception); + capturedParameter.ToArray().Should().Equal(invalidValue.ToArray()); + } + } + + public static void CustomMemoryException( + ReadOnlyMemory invalidValue, + Action, ReadOnlySpanExceptionFactory> executeAssertion + ) + { + ReadOnlyMemory capturedParameter = default; + + Exception ExceptionFactory(ReadOnlySpan parameter) + { + capturedParameter = parameter.ToArray(); + return Exception; + } + + try + { + executeAssertion(invalidValue, ExceptionFactory); + throw new XunitException("The assertion should have thrown a custom exception at this point."); + } + catch (ExceptionDummy exception) + { + exception.Should().BeSameAs(exception); + capturedParameter.ToArray().Should().Equal(invalidValue.ToArray()); + } + } + + public static void CustomException( + T1 first, + T2 second, + Action> executeAssertion + ) { T1? capturedFirst = default; T2? capturedSecond = default; @@ -73,7 +174,128 @@ Exception ExceptionFactory(T1 x, T2 y) } } - public static void CustomException(T1 first, T2 second, T3 third, Action> executeAssertion) + public static void CustomSpanException( + ReadOnlySpan invalidValue, + T2 additionalValue, + ExecuteReadOnlySpanAssertion executeAssertion + ) + { + T1[]? capturedFirst = null; + T2? capturedSecond = default; + + Exception ExceptionFactory(ReadOnlySpan parameter, T2 second) + { + capturedFirst = parameter.ToArray(); + capturedSecond = second; + return Exception; + } + + try + { + executeAssertion(invalidValue, additionalValue, ExceptionFactory); + throw new XunitException("The assertion should have thrown a custom exception at this point."); + } + catch (ExceptionDummy exception) + { + exception.Should().BeSameAs(exception); + capturedFirst.Should().Equal(invalidValue.ToArray()); + capturedSecond.Should().Be(additionalValue); + } + } + + public static void CustomSpanException( + Span invalidValue, + T2 additionalValue, + ExecuteSpanAssertion executeAssertion + ) + { + T1[]? capturedFirst = null; + T2? capturedSecond = default; + + Exception ExceptionFactory(ReadOnlySpan parameter, T2 second) + { + capturedFirst = parameter.ToArray(); + capturedSecond = second; + return Exception; + } + + try + { + executeAssertion(invalidValue, additionalValue, ExceptionFactory); + throw new XunitException("The assertion should have thrown a custom exception at this point."); + } + catch (ExceptionDummy exception) + { + exception.Should().BeSameAs(exception); + capturedFirst.Should().Equal(invalidValue.ToArray()); + capturedSecond.Should().Be(additionalValue); + } + } + + public static void CustomMemoryException( + Memory invalidValue, + T2 additionalValue, + Action, T2, ReadOnlySpanExceptionFactory> executeAssertion + ) + { + Memory capturedFirst = default; + T2? capturedSecond = default; + + Exception ExceptionFactory(ReadOnlySpan first, T2 second) + { + capturedFirst = first.ToArray(); + capturedSecond = second; + return Exception; + } + + try + { + executeAssertion(invalidValue, additionalValue, ExceptionFactory); + throw new XunitException("The assertion should have thrown a custom exception at this point."); + } + catch (ExceptionDummy exception) + { + exception.Should().BeSameAs(exception); + capturedFirst.ToArray().Should().Equal(invalidValue.ToArray()); + capturedSecond.Should().Be(additionalValue); + } + } + + public static void CustomMemoryException( + ReadOnlyMemory invalidValue, + T2 additionalValue, + Action, T2, ReadOnlySpanExceptionFactory> executeAssertion + ) + { + ReadOnlyMemory capturedFirst = default; + T2? capturedSecond = default; + + Exception ExceptionFactory(ReadOnlySpan first, T2 second) + { + capturedFirst = first.ToArray(); + capturedSecond = second; + return Exception; + } + + try + { + executeAssertion(invalidValue, additionalValue, ExceptionFactory); + throw new XunitException("The assertion should have thrown a custom exception at this point."); + } + catch (ExceptionDummy exception) + { + exception.Should().BeSameAs(exception); + capturedFirst.ToArray().Should().Equal(invalidValue.ToArray()); + capturedSecond.Should().Be(additionalValue); + } + } + + public static void CustomException( + T1 first, + T2 second, + T3 third, + Action> executeAssertion + ) { T1? capturedFirst = default; T2? capturedSecond = default; @@ -115,8 +337,9 @@ public static void CustomMessage(Action executeAssertion) wh } } - private sealed class ExceptionDummy : Exception; - - public static void WriteExceptionTo(this ExceptionAssertions exceptionAssertions, ITestOutputHelper output) where T : Exception => + public static void WriteExceptionTo(this ExceptionAssertions exceptionAssertions, ITestOutputHelper output) + where T : Exception => output.WriteLine(exceptionAssertions.Which.ToString()); -} \ No newline at end of file + + private sealed class ExceptionDummy : Exception; +} diff --git a/Code/Light.GuardClauses/Check.IsEmailAddress.cs b/Code/Light.GuardClauses/Check.IsEmailAddress.cs index 7cfb698..396e2fd 100644 --- a/Code/Light.GuardClauses/Check.IsEmailAddress.cs +++ b/Code/Light.GuardClauses/Check.IsEmailAddress.cs @@ -1,8 +1,8 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using JetBrains.Annotations; -using System.Runtime.CompilerServices; namespace Light.GuardClauses; @@ -10,7 +10,7 @@ public static partial class Check { /// /// Checks if the specified string is an email address using the default email regular expression - /// defined in . + /// defined in . /// /// The string to be checked if it is an email address. [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -23,9 +23,79 @@ public static bool IsEmailAddress([NotNullWhen(true)] this string? emailAddress) /// /// The string to be checked. /// The regular expression that determines whether the input string is an email address. - /// Thrown when is null. + /// Thrown when is null. [MethodImpl(MethodImplOptions.AggressiveInlining)] [ContractAnnotation("emailAddress:null => false; emailAddressPattern:null => halt")] public static bool IsEmailAddress([NotNullWhen(true)] this string? emailAddress, Regex emailAddressPattern) => emailAddress != null && emailAddressPattern.MustNotBeNull(nameof(emailAddressPattern)).IsMatch(emailAddress); + +#if NET8_0 + /// + /// Checks if the specified span is an email address using the default email regular expression + /// defined in . + /// + /// The span to be checked. + public static bool IsEmailAddress(this Span emailAddress) => + RegularExpressions.EmailRegex.IsMatch(emailAddress); + + /// + /// Checks if the specified span is an email address using the provided regular expression for validation. + /// + /// The span to be checked. + /// The regular expression that determines whether the input string is an email address. + /// Thrown when is null. + public static bool IsEmailAddress(this Span emailAddress, Regex emailAddressPattern) => + emailAddressPattern.MustNotBeNull(nameof(emailAddressPattern)).IsMatch(emailAddress); + + /// + /// Checks if the specified span is an email address using the default email regular expression + /// defined in . + /// + /// The span to be checked. + public static bool IsEmailAddress(this ReadOnlySpan emailAddress) => + RegularExpressions.EmailRegex.IsMatch(emailAddress); + + /// + /// Checks if the specified span is an email address using the provided regular expression for validation. + /// + /// The span to be checked. + /// The regular expression that determines whether the input string is an email address. + /// Thrown when is null. + public static bool IsEmailAddress(this ReadOnlySpan emailAddress, Regex emailAddressPattern) => + emailAddressPattern.MustNotBeNull(nameof(emailAddressPattern)).IsMatch(emailAddress); + + /// + /// Checks if the specified character memory is an email address using the default email regular expression + /// defined in . + /// + /// The character memory to be checked. + public static bool IsEmailAddress(this Memory emailAddress) => + RegularExpressions.EmailRegex.IsMatch(emailAddress.Span); + + /// + /// Checks if the specified character memory is an email address using the provided regular expression for validation. + /// + /// The character memory to be checked. + /// The regular expression that determines whether the input string is an email address. + /// Thrown when is null. + public static bool IsEmailAddress(this Memory emailAddress, Regex emailAddressPattern) => + emailAddressPattern.MustNotBeNull(nameof(emailAddressPattern)).IsMatch(emailAddress.Span); + + /// + /// Checks if the specified character memory is an email address using the default email regular expression + /// defined in . + /// + /// The character memory to be checked. + public static bool IsEmailAddress(this ReadOnlyMemory emailAddress) => + RegularExpressions.EmailRegex.IsMatch(emailAddress.Span); + + /// + /// Checks if the specified character memory is an email address using the provided regular expression for validation. + /// + /// The character memory to be checked. + /// The regular expression that determines whether the input string is an email address. + /// Thrown when is null. + public static bool IsEmailAddress(this ReadOnlyMemory emailAddress, Regex emailAddressPattern) => + emailAddressPattern.MustNotBeNull(nameof(emailAddressPattern)).IsMatch(emailAddress.Span); +#endif } diff --git a/Code/Light.GuardClauses/Check.MustBeEmailAddress.cs b/Code/Light.GuardClauses/Check.MustBeEmailAddress.cs index 920e55e..ef2b33e 100644 --- a/Code/Light.GuardClauses/Check.MustBeEmailAddress.cs +++ b/Code/Light.GuardClauses/Check.MustBeEmailAddress.cs @@ -107,4 +107,337 @@ public static string MustBeEmailAddress( return parameter; } + +#if NET8_0 + /// + /// Ensures that the span represents a valid email address using the default email regular expression + /// defined in , or otherwise throws an . + /// + /// The span that will be validated as an email address. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when is no valid email address. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan MustBeEmailAddress( + this ReadOnlySpan parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (!parameter.IsEmailAddress()) + { + Throw.InvalidEmailAddress(parameter, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the span represents a valid email address using the default email regular expression + /// defined in , or otherwise throws your custom exception. + /// + /// The span that will be validated as an email address. + /// The delegate that creates your custom exception. A string created from is passed to this delegate. + /// Your custom exception thrown when is no valid email address. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan MustBeEmailAddress( + this ReadOnlySpan parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + if (!parameter.IsEmailAddress()) + { + Throw.CustomSpanException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the span represents a valid email address using the provided regular expression, + /// or otherwise throws an . + /// + /// The span that will be validated as an email address. + /// The regular expression that determines if the span represents a valid email. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when is no valid email address. + /// Thrown when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("emailAddressPattern:null => halt")] + public static ReadOnlySpan MustBeEmailAddress( + this ReadOnlySpan parameter, + Regex emailAddressPattern, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + emailAddressPattern.MustNotBeNull(nameof(emailAddressPattern)); + + if (!parameter.IsEmailAddress(emailAddressPattern)) + { + Throw.InvalidEmailAddress(parameter, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the span represents a valid email address using the provided regular expression, + /// or otherwise throws your custom exception. + /// + /// The span that will be validated as an email address. + /// The regular expression that determines if the span represents a valid email. + /// The delegate that creates your custom exception. A string created from and are passed to this delegate. + /// Your custom exception thrown when is no valid email address or when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan MustBeEmailAddress( + this ReadOnlySpan parameter, + Regex emailAddressPattern, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract -- caller might have NRTs turned off + if (emailAddressPattern is null || !parameter.IsEmailAddress(emailAddressPattern)) + { + Throw.CustomSpanException(exceptionFactory, parameter, emailAddressPattern!); + } + + return parameter; + } + + /// + /// Ensures that the span represents a valid email address using the default email regular expression + /// defined in , or otherwise throws an . + /// + /// The span that will be validated as an email address. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when is no valid email address. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeEmailAddress( + this Span parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter).MustBeEmailAddress(parameterName, message); + return parameter; + } + + /// + /// Ensures that the span represents a valid email address using the default email regular expression + /// defined in , or otherwise throws your custom exception. + /// + /// The span that will be validated as an email address. + /// The delegate that creates your custom exception. A string created from is passed to this delegate. + /// Your custom exception thrown when is no valid email address. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeEmailAddress( + this Span parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + ((ReadOnlySpan) parameter).MustBeEmailAddress(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the span represents a valid email address using the provided regular expression, + /// or otherwise throws an . + /// + /// The span that will be validated as an email address. + /// The regular expression that determines if the span represents a valid email. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when is no valid email address. + /// Thrown when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("emailAddressPattern:null => halt")] + public static Span MustBeEmailAddress( + this Span parameter, + Regex emailAddressPattern, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter).MustBeEmailAddress(emailAddressPattern, parameterName, message); + return parameter; + } + + /// + /// Ensures that the span represents a valid email address using the provided regular expression, + /// or otherwise throws your custom exception. + /// + /// The span that will be validated as an email address. + /// The regular expression that determines if the span represents a valid email. + /// The delegate that creates your custom exception. A string created from and are passed to this delegate. + /// Your custom exception thrown when is no valid email address or when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span MustBeEmailAddress( + this Span parameter, + Regex emailAddressPattern, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + ((ReadOnlySpan) parameter).MustBeEmailAddress(emailAddressPattern, exceptionFactory); + return parameter; + } + + /// + /// Ensures that the memory represents a valid email address using the default email regular expression + /// defined in , or otherwise throws an . + /// + /// The memory that will be validated as an email address. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when is no valid email address. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeEmailAddress( + this Memory parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter.Span).MustBeEmailAddress(parameterName, message); + return parameter; + } + + /// + /// Ensures that the memory represents a valid email address using the default email regular expression + /// defined in , or otherwise throws your custom exception. + /// + /// The memory that will be validated as an email address. + /// The delegate that creates your custom exception. A string created from is passed to this delegate. + /// Your custom exception thrown when is no valid email address. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeEmailAddress( + this Memory parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + ((ReadOnlySpan) parameter.Span).MustBeEmailAddress(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the memory represents a valid email address using the provided regular expression, + /// or otherwise throws an . + /// + /// The memory that will be validated as an email address. + /// The regular expression that determines if the memory represents a valid email. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when is no valid email address. + /// Thrown when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("emailAddressPattern:null => halt")] + public static Memory MustBeEmailAddress( + this Memory parameter, + Regex emailAddressPattern, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + ((ReadOnlySpan) parameter.Span).MustBeEmailAddress(emailAddressPattern, parameterName, message); + return parameter; + } + + /// + /// Ensures that the memory represents a valid email address using the provided regular expression, + /// or otherwise throws your custom exception. + /// + /// The memory that will be validated as an email address. + /// The regular expression that determines if the memory represents a valid email. + /// The delegate that creates your custom exception. A string created from and are passed to this delegate. + /// Your custom exception thrown when is no valid email address or when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory MustBeEmailAddress( + this Memory parameter, + Regex emailAddressPattern, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + ((ReadOnlySpan) parameter.Span).MustBeEmailAddress(emailAddressPattern, exceptionFactory); + return parameter; + } + + /// + /// Ensures that the memory represents a valid email address using the default email regular expression + /// defined in , or otherwise throws an . + /// + /// The memory that will be validated as an email address. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when is no valid email address. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeEmailAddress( + this ReadOnlyMemory parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + parameter.Span.MustBeEmailAddress(parameterName, message); + return parameter; + } + + /// + /// Ensures that the memory represents a valid email address using the default email regular expression + /// defined in , or otherwise throws your custom exception. + /// + /// The memory that will be validated as an email address. + /// The delegate that creates your custom exception. A string created from is passed to this delegate. + /// Your custom exception thrown when is no valid email address. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeEmailAddress( + this ReadOnlyMemory parameter, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + parameter.Span.MustBeEmailAddress(exceptionFactory); + return parameter; + } + + /// + /// Ensures that the memory represents a valid email address using the provided regular expression, + /// or otherwise throws an . + /// + /// The memory that will be validated as an email address. + /// The regular expression that determines if the memory represents a valid email. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when is no valid email address. + /// Thrown when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("emailAddressPattern:null => halt")] + public static ReadOnlyMemory MustBeEmailAddress( + this ReadOnlyMemory parameter, + Regex emailAddressPattern, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + parameter.Span.MustBeEmailAddress(emailAddressPattern, parameterName, message); + return parameter; + } + + /// + /// Ensures that the memory represents a valid email address using the provided regular expression, + /// or otherwise throws your custom exception. + /// + /// The memory that will be validated as an email address. + /// The regular expression that determines if the memory represents a valid email. + /// The delegate that creates your custom exception. A string created from and are passed to this delegate. + /// Your custom exception thrown when is no valid email address or when is null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory MustBeEmailAddress( + this ReadOnlyMemory parameter, + Regex emailAddressPattern, + ReadOnlySpanExceptionFactory exceptionFactory + ) + { + parameter.Span.MustBeEmailAddress(emailAddressPattern, exceptionFactory); + return parameter; + } +#endif } diff --git a/Code/Light.GuardClauses/Exceptions/Throw.cs b/Code/Light.GuardClauses/Exceptions/Throw.cs index 7df3649..104c6b2 100644 --- a/Code/Light.GuardClauses/Exceptions/Throw.cs +++ b/Code/Light.GuardClauses/Exceptions/Throw.cs @@ -84,6 +84,14 @@ public static void Argument(string? parameterName = null, string? message = null [DoesNotReturn] public static void InvalidEmailAddress(string parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) => throw new InvalidEmailAddressException(parameterName, message ?? $"{parameterName ?? "The string"} must be a valid email address, but it actually is \"{parameter}\"."); + + /// + /// Throws an using the optional message. + /// + [ContractAnnotation("=> halt")] + [DoesNotReturn] + public static void InvalidEmailAddress(ReadOnlySpan parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) => + throw new InvalidEmailAddressException(parameterName, message ?? $"{parameterName ?? "The string"} must be a valid email address, but it actually is \"{parameter.ToString()}\"."); /// /// Throws the default indicating that a has no value, using the optional parameter name and message. diff --git a/Code/Light.GuardClauses/RegularExpressions.cs b/Code/Light.GuardClauses/RegularExpressions.cs index 3fec571..238f17c 100644 --- a/Code/Light.GuardClauses/RegularExpressions.cs +++ b/Code/Light.GuardClauses/RegularExpressions.cs @@ -14,8 +14,9 @@ public static class RegularExpressions /// /// Gets the string that represents the . /// + // This is an AI-generated regex. I don't have any clue and I find it way to complex to ever understand it. public const string EmailRegexText = - @"^[\w!#$%&'*+\-/=?\^_`{|}~]+(\.[\w!#$%&'*+\-/=?\^_`{|}~]+)*@((((\w+\-?)+\.)+[a-zA-Z]{2,4})|(([0-9]{1,3}\.){3}[0-9]{1,3}))$"; + @"^(?:(?:""(?:(?:[^""\\]|\\.)*)""|[\p{L}\p{N}!#$%&'*+\-/=?^_`{|}~-]+(?:\.[\p{L}\p{N}!#$%&'*+\-/=?^_`{|}~-]+)*)@(?:(?:[A-Za-z0-9](?:[A-Za-z0-9\-]*[A-Za-z0-9])?\.)+[A-Za-z]{2,}|(?:\[(?:IPv6:[0-9A-Fa-f:.]+)\])|(?:25[0-5]|2[0-4]\d|[01]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d?\d)){3}))$"; /// /// Gets the default regular expression for email validation.