Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace ktsu.Semantics.Strings;

using System;
using System.Linq;

/// <summary>
/// Validates that a string is in kebab-case (lowercase words separated by hyphens)
Expand Down Expand Up @@ -34,38 +33,7 @@ private sealed class KebabCaseValidator : ValidationAdapter
/// </summary>
/// <param name="value">The string value to validate</param>
/// <returns>A validation result indicating success or failure</returns>
protected override ValidationResult ValidateValue(string value)
{
if (string.IsNullOrEmpty(value))
{
return ValidationResult.Success();
}

// Cannot start or end with hyphen
if (value.StartsWith('-') || value.EndsWith('-'))
{
return ValidationResult.Failure(FailureMessage);
}

// Cannot have consecutive hyphens
if (value.Contains("--"))
{
return ValidationResult.Failure(FailureMessage);
}

// No spaces, underscores, or other separators allowed (except hyphens)
if (value.Any(c => char.IsWhiteSpace(c) || c == '_'))
{
return ValidationResult.Failure(FailureMessage);
}

// All characters must be lowercase letters, digits, or hyphens
if (!value.All(c => char.IsLower(c) || char.IsDigit(c) || c == '-'))
{
return ValidationResult.Failure(FailureMessage);
}

return ValidationResult.Success();
}
protected override ValidationResult ValidateValue(string value) =>
DelimitedCaseValidation.Validate(value, '-', '_', char.IsLower, FailureMessage);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace ktsu.Semantics.Strings;

using System;
using System.Linq;

/// <summary>
/// Validates that a string is in MACRO_CASE (uppercase words separated by underscores)
Expand Down Expand Up @@ -35,38 +34,7 @@ private sealed class MacroCaseValidator : ValidationAdapter
/// </summary>
/// <param name="value">The string value to validate</param>
/// <returns>A validation result indicating success or failure</returns>
protected override ValidationResult ValidateValue(string value)
{
if (string.IsNullOrEmpty(value))
{
return ValidationResult.Success();
}

// Cannot start or end with underscore
if (value.StartsWith('_') || value.EndsWith('_'))
{
return ValidationResult.Failure(FailureMessage);
}

// Cannot have consecutive underscores
if (value.Contains("__"))
{
return ValidationResult.Failure(FailureMessage);
}

// No spaces, hyphens, or other separators allowed (except underscores)
if (value.Any(c => char.IsWhiteSpace(c) || c == '-'))
{
return ValidationResult.Failure(FailureMessage);
}

// All characters must be uppercase letters, digits, or underscores
if (!value.All(c => char.IsUpper(c) || char.IsDigit(c) || c == '_'))
{
return ValidationResult.Failure(FailureMessage);
}

return ValidationResult.Success();
}
protected override ValidationResult ValidateValue(string value) =>
DelimitedCaseValidation.Validate(value, '_', '-', char.IsUpper, FailureMessage);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace ktsu.Semantics.Strings;

using System;
using System.Linq;

/// <summary>
/// Validates that a string is in snake_case (lowercase words separated by underscores)
Expand Down Expand Up @@ -34,38 +33,7 @@ private sealed class SnakeCaseValidator : ValidationAdapter
/// </summary>
/// <param name="value">The string value to validate</param>
/// <returns>A validation result indicating success or failure</returns>
protected override ValidationResult ValidateValue(string value)
{
if (string.IsNullOrEmpty(value))
{
return ValidationResult.Success();
}

// Cannot start or end with underscore
if (value.StartsWith('_') || value.EndsWith('_'))
{
return ValidationResult.Failure(FailureMessage);
}

// Cannot have consecutive underscores
if (value.Contains("__"))
{
return ValidationResult.Failure(FailureMessage);
}

// No spaces, hyphens, or other separators allowed (except underscores)
if (value.Any(c => char.IsWhiteSpace(c) || c == '-'))
{
return ValidationResult.Failure(FailureMessage);
}

// All characters must be lowercase letters, digits, or underscores
if (!value.All(c => char.IsLower(c) || char.IsDigit(c) || c == '_'))
{
return ValidationResult.Failure(FailureMessage);
}

return ValidationResult.Success();
}
protected override ValidationResult ValidateValue(string value) =>
DelimitedCaseValidation.Validate(value, '_', '-', char.IsLower, FailureMessage);
}
}
62 changes: 62 additions & 0 deletions Semantics.Strings/Validation/DelimitedCaseValidation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Semantics.Strings;

using System;
using System.Linq;

/// <summary>
/// The shared rules for the delimiter-separated casing conventions — kebab-case, snake_case and
/// MACRO_CASE — which differ only in the delimiter they use, the delimiter they forbid, and the
/// letter case they require.
/// </summary>
internal static class DelimitedCaseValidation
{
/// <summary>
/// Validates a delimiter-separated casing convention.
/// </summary>
/// <param name="value">The string value to validate.</param>
/// <param name="delimiter">The delimiter the convention separates words with.</param>
/// <param name="foreignDelimiter">The delimiter belonging to a different convention, which must not appear.</param>
/// <param name="isExpectedCase">Predicate matching the letter case the convention requires.</param>
/// <param name="failureMessage">The message reported for every way a value can fail.</param>
/// <returns>A validation result indicating success or failure.</returns>
internal static ValidationResult Validate(
string value,
char delimiter,
char foreignDelimiter,
Func<char, bool> isExpectedCase,
string failureMessage)
{
if (string.IsNullOrEmpty(value))
{
return ValidationResult.Success();
}

// Cannot start or end with the delimiter.
if (value.StartsWith(delimiter) || value.EndsWith(delimiter))
{
return ValidationResult.Failure(failureMessage);
}

// Cannot have consecutive delimiters.
for (int i = 1; i < value.Length; i++)
{
if (value[i] == delimiter && value[i - 1] == delimiter)
{
return ValidationResult.Failure(failureMessage);
}
}

// No whitespace, and no delimiter belonging to a different convention.
if (value.Any(c => char.IsWhiteSpace(c) || c == foreignDelimiter))
{
return ValidationResult.Failure(failureMessage);
}

// Every character must be an expected-case letter, a digit, or the delimiter.
return value.All(c => isExpectedCase(c) || char.IsDigit(c) || c == delimiter)
? ValidationResult.Success()
: ValidationResult.Failure(failureMessage);
}
}
Loading