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()