diff --git a/Semantics.Strings/Validation/Attributes/Text/PrefixAndSuffixAttribute.cs b/Semantics.Strings/Validation/Attributes/Text/PrefixAndSuffixAttribute.cs index ade015e4..8e65bffd 100644 --- a/Semantics.Strings/Validation/Attributes/Text/PrefixAndSuffixAttribute.cs +++ b/Semantics.Strings/Validation/Attributes/Text/PrefixAndSuffixAttribute.cs @@ -5,7 +5,8 @@ namespace ktsu.Semantics.Strings; using System; /// -/// Validates that the string has both the specified prefix and suffix +/// Validates that the string has both the specified prefix and suffix, without the two overlapping. +/// An empty string fails, as it does for and . /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] public sealed class PrefixAndSuffixAttribute(string prefix, string suffix, StringComparison comparison = StringComparison.Ordinal) : NativeSemanticStringValidationAttribute @@ -55,7 +56,13 @@ protected override ValidationResult ValidateValue(string value) { if (string.IsNullOrEmpty(value)) { - return ValidationResult.Success(); + return ValidationResult.Failure($"The value must start with '{_prefix}' and end with '{_suffix}'."); + } + + // The prefix and suffix must each have their own characters, so one '"' cannot be both quotes + if (value.Length < _prefix.Length + _suffix.Length) + { + return ValidationResult.Failure($"The value must start with '{_prefix}' and end with '{_suffix}' without the two overlapping."); } // Check prefix diff --git a/Semantics.Test/TextValidationAttributesTests.cs b/Semantics.Test/TextValidationAttributesTests.cs index 1e49d1db..f0ab5993 100644 --- a/Semantics.Test/TextValidationAttributesTests.cs +++ b/Semantics.Test/TextValidationAttributesTests.cs @@ -196,6 +196,48 @@ public void PrefixAndSuffixAttribute_MissingBoth_ShouldThrow() Assert.ThrowsExactly(() => SemanticString.Create("middle")); } + [PrefixAndSuffix("\"", "\"")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Used via generic type references")] + private sealed partial record QuotedTestString : SemanticString { } + + [PrefixAndSuffix("ab", "ba")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Used via generic type references")] + private sealed partial record AbBaTestString : SemanticString { } + + [TestMethod] + public void PrefixAndSuffixAttribute_LoneDelimiterServingAsBoth_ShouldThrow() + { + Assert.ThrowsExactly(() => SemanticString.Create("\"")); + } + + [TestMethod] + public void PrefixAndSuffixAttribute_OverlappingPrefixAndSuffix_ShouldThrow() + { + Assert.ThrowsExactly(() => SemanticString.Create("aba")); + } + + [TestMethod] + public void PrefixAndSuffixAttribute_EmptyString_ShouldThrow() + { + Assert.ThrowsExactly(() => SemanticString.Create(string.Empty)); + } + + [TestMethod] + [DataRow("\"\"")] + [DataRow("\"quoted\"")] + public void PrefixAndSuffixAttribute_DelimitersEachPresent_ShouldPass(string value) + { + QuotedTestString quoted = SemanticString.Create(value); + Assert.AreEqual(value, quoted.WeakString); + } + + [TestMethod] + public void PrefixAndSuffixAttribute_AdjacentPrefixAndSuffix_ShouldPass() + { + AbBaTestString value = SemanticString.Create("abba"); + Assert.AreEqual("abba", value.WeakString); + } + // Test multiple validation attributes on empty strings [TestMethod] public void ValidationAttributes_EmptyString_ShouldHandleCorrectly()