Skip to content

Commit

Permalink
Merge pull request #2 from WhitWaldo/string-constraint
Browse files Browse the repository at this point in the history
Updated string constraint extension
  • Loading branch information
rudiv authored May 18, 2024
2 parents bc4a7fd + 0f8460c commit 41b9def
Show file tree
Hide file tree
Showing 3 changed files with 244 additions and 42 deletions.
26 changes: 16 additions & 10 deletions Achieve.Aspire.sln.DotSettings.user
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,23 @@
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue">&lt;AssemblyExplorer&gt;
&lt;Assembly Path="/Users/rudi/.nuget/packages/azure.provisioning.keyvault/0.1.0-beta.1/lib/netstandard2.0/Azure.Provisioning.KeyVault.dll" /&gt;
&lt;/AssemblyExplorer&gt;</s:String>
<s:String x:Key="/Default/Environment/Highlighting/HighlightingSourceSnapshotLocation/@EntryValue">/Users/rudi/Library/Caches/JetBrains/Rider2024.1/resharper-host/temp/Rider/vAny/CoverageData/_Achieve.Aspire.-201606746/Snapshot/snapshot.utdcvr</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=8f063988_002D5874_002D455c_002D8e8b_002Dad9978e141a6/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="ResourceGeneratesCorrectHeader" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
&lt;Solution /&gt;

<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=8f063988_002D5874_002D455c_002D8e8b_002Dad9978e141a6/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="ResourceGeneratesCorrectHeader" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;Solution /&gt;&#xD;
&lt;/SessionState&gt;</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=b5be586d_002Dd334_002D4bb7_002D8da4_002D0a4a9f3acd1a/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="TestManagedIdentityAddition" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
&lt;TestAncestor&gt;
&lt;TestId&gt;xUnit::6BB73DF6-CBD9-4B8E-8717-73ACA5FE9A44::net8.0::Achieve.Aspire.AzureProvisioning.Tests.IdentityTests&lt;/TestId&gt;
&lt;TestId&gt;xUnit::6BB73DF6-CBD9-4B8E-8717-73ACA5FE9A44::net8.0::Achieve.Aspire.AzureProvisioning.Tests.KeyVaultTests&lt;/TestId&gt;
&lt;TestId&gt;xUnit::6BB73DF6-CBD9-4B8E-8717-73ACA5FE9A44::net8.0::Achieve.Aspire.AzureProvisioning.Tests.Bicep.BasicGeneratorTests&lt;/TestId&gt;
&lt;TestId&gt;xUnit::6BB73DF6-CBD9-4B8E-8717-73ACA5FE9A44::net8.0::Achieve.Aspire.AzureProvisioning.Tests.Bicep.ResourceGeneratorTests&lt;/TestId&gt;
&lt;/TestAncestor&gt;
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=b5be586d_002Dd334_002D4bb7_002D8da4_002D0a4a9f3acd1a/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" Name="TestManagedIdentityAddition" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;Or&gt;&#xD;
&lt;TestAncestor&gt;&#xD;
&lt;TestId&gt;xUnit::6BB73DF6-CBD9-4B8E-8717-73ACA5FE9A44::net8.0::Achieve.Aspire.AzureProvisioning.Tests.IdentityTests&lt;/TestId&gt;&#xD;
&lt;TestId&gt;xUnit::6BB73DF6-CBD9-4B8E-8717-73ACA5FE9A44::net8.0::Achieve.Aspire.AzureProvisioning.Tests.KeyVaultTests&lt;/TestId&gt;&#xD;
&lt;TestId&gt;xUnit::6BB73DF6-CBD9-4B8E-8717-73ACA5FE9A44::net8.0::Achieve.Aspire.AzureProvisioning.Tests.Bicep.BasicGeneratorTests&lt;/TestId&gt;&#xD;
&lt;TestId&gt;xUnit::6BB73DF6-CBD9-4B8E-8717-73ACA5FE9A44::net8.0::Achieve.Aspire.AzureProvisioning.Tests.Bicep.ResourceGeneratorTests&lt;/TestId&gt;&#xD;
&lt;TestId&gt;xUnit::6BB73DF6-CBD9-4B8E-8717-73ACA5FE9A44::net8.0::Achieve.Aspire.AzureProvisioning.Tests.BatchTests.AzureProvisionerIsAdded&lt;/TestId&gt;&#xD;
&lt;TestId&gt;xUnit::6BB73DF6-CBD9-4B8E-8717-73ACA5FE9A44::net8.0::Achieve.Aspire.AzureProvisioning.Tests.BatchTests.BasicBatchGeneratesCorrectly&lt;/TestId&gt;&#xD;
&lt;TestId&gt;xUnit::6BB73DF6-CBD9-4B8E-8717-73ACA5FE9A44::net8.0::Achieve.Aspire.AzureProvisioning.Tests.BatchTests&lt;/TestId&gt;&#xD;
&lt;/TestAncestor&gt;&#xD;
&lt;ProjectFile&gt;6BB73DF6-CBD9-4B8E-8717-73ACA5FE9A44/d:Extensions/f:StringExtensionTests.cs&lt;/ProjectFile&gt;&#xD;
&lt;/Or&gt;&#xD;
&lt;/SessionState&gt;</s:String>

</wpf:ResourceDictionary>
121 changes: 90 additions & 31 deletions src/Achieve.Aspire.AzureProvisioning/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

using Microsoft.WindowsAzure.ResourceStack.Common.Extensions;

namespace Achieve.Aspire.AzureProvisioning.Extensions;

Expand All @@ -11,17 +11,52 @@ internal static class StringExtensions
/// Denotes classes of character types.
/// </summary>
[Flags]
internal enum CharacterClass
public enum CharacterClass
{
UppercaseLetter = 0b_000001,
LowercaseLetter = 0b_000010,
Underscore = 0b_000100,
Hyphen = 0b_001000,
Whitespace = 0b_010000,
Number = 0b_100000,
/// <summary>
/// Reflects an uppercase letter character.
/// </summary>
UppercaseLetter = 1,
/// <summary>
/// Reflects a lowercase letter character.
/// </summary>
LowercaseLetter = 2,
/// <summary>
/// Reflects an underscore character.
/// </summary>
Underscore = 4,
/// <summary>
/// Reflects a hyphen character.
/// </summary>
Hyphen = 8,
/// <summary>
/// Reflects a whitespace character.
/// </summary>
Whitespace = 16,
/// <summary>
/// Reflects a number.
/// </summary>
Number = 32,
/// <summary>
/// Reflects a period.
/// </summary>
Period = 64,
/// <summary>
/// Reflects either open or close parentheses.
/// </summary>
Parentheses = 128,
/// <summary>
/// Reflects an alphabetic character.
/// </summary>
Alphabetic = UppercaseLetter | LowercaseLetter,
Alphanumeric = Alphabetic | Number,
Any = UppercaseLetter | LowercaseLetter | Number | Underscore | Hyphen | Whitespace
/// <summary>
/// Reflects an alphanumeric character.
/// </summary>
Alphanumeric = Alphabetic | Number,
/// <summary>
/// Reflects any character as being valid.
/// </summary>
Any = UppercaseLetter | LowercaseLetter | Number | Underscore | Hyphen | Whitespace | Period | Parentheses
}

/// <summary>
Expand All @@ -30,35 +65,59 @@ internal enum CharacterClass
/// <param name="str">The string to evaluate.</param>
/// <param name="minLength">The minimum allowed length of the string.</param>
/// <param name="maxLength">The maximum allowed length of the string.</param>
/// <param name="characterClasses">The types of characters allowed in the string.</param>
/// <param name="contains">The types of characters allowed in the string.</param>
/// <param name="doesNotContain">The types of characters not allowed in the string.</param>
/// <param name="startsWith">The types of characters allowed for the string to start with, if any.</param>
/// <param name="doesNotStartWith">The types of characters the string is not allowed to start with, if any.</param>
/// <param name="endsWith">The types of characters allowed for the string to end with, if any.</param>
/// <param name="doesNotEndWith">The types of characters the string is not allowed to end with, if any.</param>
/// <returns>True if the string matches the provided constraints; otherwise false.</returns>
public static bool MatchesConstraints(this string str, int? minLength, int? maxLength, CharacterClass characterClasses)
public static bool MatchesConstraints(this string str, int? minLength = null, int? maxLength = null, CharacterClass? contains = null, CharacterClass? doesNotContain = null, CharacterClass? startsWith = null, CharacterClass? doesNotStartWith = null, CharacterClass? endsWith = null, CharacterClass? doesNotEndWith = null)
{
if ((minLength != null && str.Length < minLength) || (maxLength != null && str.Length > maxLength))
//Validate the length constraints
if ((minLength is not null && str.Length < minLength) || (maxLength is not null && str.Length > maxLength))
return false;

if (characterClasses.HasFlag(CharacterClass.Any))
//If there's no string length and we've otherwise met the length constraints, skip the remaining checks
if (str.Length == 0)
return true;

//Create a set of allowed characters based on the flags
var allowedChars = new HashSet<char>();

if (characterClasses.HasFlag(CharacterClass.UppercaseLetter))
allowedChars.UnionWith("ABCDEFGHIJKLMNOPQRSTUVWXYZ");

if (characterClasses.HasFlag(CharacterClass.LowercaseLetter))
allowedChars.UnionWith("abcdefghijklmnopqrstuvwxyz");
return ValidateConstraint(str.First().ToString(), startsWith) &&
ValidateConstraint(str.First().ToString(), doesNotStartWith, true) &&
ValidateConstraint(str.Last().ToString(), endsWith) &&
ValidateConstraint(str.Last().ToString(), doesNotEndWith, true) &&
ValidateConstraint(str, contains) &&
ValidateConstraint(str, doesNotContain, true);

bool ValidateConstraint(string value, CharacterClass? constraint, bool checkAsNot = false)
{
if (constraint == null)
return true;

var allowedChars = new HashSet<char>();

if (((CharacterClass)constraint).HasFlag(CharacterClass.UppercaseLetter))
allowedChars.UnionWith("ABCDEFGHIJKLMNOPQRSTUVWXYZ");

if (((CharacterClass)constraint).HasFlag(CharacterClass.LowercaseLetter))
allowedChars.UnionWith("abcdefghijklmnopqrstuvwxyz");

if (((CharacterClass)constraint).HasFlag(CharacterClass.Number))
allowedChars.UnionWith("0123456789");

if (((CharacterClass)constraint).HasFlag(CharacterClass.Underscore))
allowedChars.Add('_');

if (((CharacterClass)constraint).HasFlag(CharacterClass.Hyphen))
allowedChars.Add('-');

if (characterClasses.HasFlag(CharacterClass.Number))
allowedChars.UnionWith("0123456789");
if (((CharacterClass)constraint).HasFlag(CharacterClass.Period))
allowedChars.Add('.');

if (characterClasses.HasFlag(CharacterClass.Underscore))
allowedChars.Add('_');
if (((CharacterClass) constraint).HasFlag(CharacterClass.Parentheses))
allowedChars.UnionWith(['(', ')']);

if (characterClasses.HasFlag(CharacterClass.Hyphen))
allowedChars.Add('-');

// Check each character in the input string
return str.All(c => allowedChars.Contains(c));
return value.All(c => allowedChars.Contains(c) == !checkAsNot);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ namespace Achieve.Aspire.AzureProvisioning.Tests.Extensions;

public class StringExtensionTests
{
[Fact]
public void ShouldPassIfNoConstraintsSpecified()
{
const string test = "abc123";
var result = test.MatchesConstraints();
Assert.True(result);
}

[Fact]
public void ShouldFailWhenLengthIsTooShort()
{
Expand All @@ -21,7 +29,7 @@ public void ShouldFailWhenLengthIsTooLong()
}

[Fact]
public void ShoudlAcceptValidLength()
public void ShouldAcceptValidLength()
{
const string test = "ThisIsATest";
var result = test.MatchesConstraints(3, 25, StringExtensions.CharacterClass.Any);
Expand Down Expand Up @@ -51,6 +59,14 @@ public void ShouldNotAcceptNonAlphabeticCharacters()
var result = test.MatchesConstraints(2, 25, StringExtensions.CharacterClass.Alphabetic);
Assert.False(result);
}

[Fact]
public void ShouldNotAllowAlphabeticCharacters()
{
const string test = "!@# #$%";
var result = test.MatchesConstraints(doesNotContain: StringExtensions.CharacterClass.Alphabetic);
Assert.True(result);
}

[Fact]
public void ShouldAcceptAlphanumericCharacters()
Expand All @@ -76,6 +92,14 @@ public void ShouldOnlyAllowAlphaNumeric()
StringExtensions.CharacterClass.LowercaseLetter | StringExtensions.CharacterClass.UppercaseLetter | StringExtensions.CharacterClass.Number);
Assert.True(result);
}

[Fact]
public void ShouldNotAllowAlphaNumeric()
{
const string test = "!@#$%";
var result = test.MatchesConstraints(doesNotContain: StringExtensions.CharacterClass.Alphanumeric);
Assert.True(result);
}

[Fact]
public void ShouldFailWhenExpectingOnlyAlphaNumeric()
Expand All @@ -102,11 +126,124 @@ public void ShouldAllowUnderscores()
Assert.True(result);
}

[Fact]
public void ShouldNotAllowUnderscores()
{
const string test = "test_893";
var result = test.MatchesConstraints(doesNotContain: StringExtensions.CharacterClass.Underscore);
Assert.False(result);
}

[Fact]
public void ShouldAllowHyphens()
{
const string test = "test--993";
var result = test.MatchesConstraints(3, 25, StringExtensions.CharacterClass.LowercaseLetter | StringExtensions.CharacterClass.Hyphen | StringExtensions.CharacterClass.Number);
Assert.True(result);
}

[Fact]
public void ShouldNotAllowPeriods()
{
const string test = "test.93";
var result = test.MatchesConstraints(3, 25, StringExtensions.CharacterClass.Alphanumeric);
Assert.False(result);
}

[Fact]
public void ShouldAllowPeriods()
{
const string test = "test.93";
var result = test.MatchesConstraints(3, 25, StringExtensions.CharacterClass.Alphanumeric | StringExtensions.CharacterClass.Period);
Assert.True(result);
}

[Fact]
public void ShouldNotAllowParentheses()
{
const string test = "test(93)";
var result = test.MatchesConstraints(3, 25, StringExtensions.CharacterClass.Alphanumeric);
Assert.False(result);
}

[Fact]
public void ShouldAllowParentheses()
{
const string test = "test(93)";
var result = test.MatchesConstraints(3, 25,
StringExtensions.CharacterClass.Alphanumeric | StringExtensions.CharacterClass.Parentheses);
Assert.True(result);
}

[Fact]
public void ShouldValidateStartingCharacterAsSuccess()
{
const string test = "test--993";
var result = test.MatchesConstraints(3, 25,
StringExtensions.CharacterClass.LowercaseLetter | StringExtensions.CharacterClass.Hyphen | StringExtensions.CharacterClass.Number,
startsWith: StringExtensions.CharacterClass.Number | StringExtensions.CharacterClass.LowercaseLetter);
Assert.True(result);
}

[Fact]
public void ShouldValidateStartingCharacterAsFailure()
{
const string test = "test--993";
var result = test.MatchesConstraints(3, 25,
StringExtensions.CharacterClass.LowercaseLetter | StringExtensions.CharacterClass.Hyphen | StringExtensions.CharacterClass.Number,
startsWith: StringExtensions.CharacterClass.Number);
Assert.False(result);
}

[Fact]
public void ShouldFailSinceTheValueShouldNotStartWithANumber()
{
const string test = "23test";
var result = test.MatchesConstraints(doesNotStartWith: StringExtensions.CharacterClass.Number);
Assert.False(result);
}

[Fact]
public void ShouldPassSinceTheValueDoesNotStartWithANumber()
{
const string test = "test32";
var result = test.MatchesConstraints(doesNotStartWith: StringExtensions.CharacterClass.Number);
Assert.True(result);
}

[Fact]
public void ShouldValidateEndingCharacterAsSuccess()
{
const string test = "test--993";
var result = test.MatchesConstraints(3, 25,
StringExtensions.CharacterClass.LowercaseLetter | StringExtensions.CharacterClass.Hyphen | StringExtensions.CharacterClass.Number,
endsWith: StringExtensions.CharacterClass.Alphanumeric);
Assert.True(result);
}

[Fact]
public void ShouldFailAsTheValueShouldNotEndWithNumbers()
{
const string test = "test-33";
var result = test.MatchesConstraints(doesNotEndWith: StringExtensions.CharacterClass.Number);
Assert.False(result);
}

[Fact]
public void ShouldPassSinceTheValueDoesNotEndWithNumbers()
{
const string test = "test-33a";
var result = test.MatchesConstraints(doesNotEndWith: StringExtensions.CharacterClass.Number);
Assert.True(result);
}

[Fact]
public void ShouldValidateEndingCharacterAsFailure()
{
const string test = "test--993";
var result = test.MatchesConstraints(3, 25,
StringExtensions.CharacterClass.LowercaseLetter | StringExtensions.CharacterClass.Hyphen | StringExtensions.CharacterClass.Number,
endsWith: StringExtensions.CharacterClass.Alphabetic);
Assert.False(result);
}
}

0 comments on commit 41b9def

Please sign in to comment.