Files
GmRelayBot/src/GmRelay.Web/Services/Portfolio/PortfolioValidation.cs
T

153 lines
4.3 KiB
C#

using System.Text;
namespace GmRelay.Web.Services.Portfolio;
public static class PortfolioValidation
{
private const int MinSlugLength = 3;
private const int MaxSlugLength = 160;
private const int MinTitleLength = 2;
private const int MaxTitleLength = 255;
private const int MaxDescriptionLength = 5000;
private const int MinReviewBodyLength = 10;
private const int MaxReviewBodyLength = 2000;
private static readonly HashSet<string> AllowedFormats = new(StringComparer.Ordinal)
{
"Online",
"Offline",
"Hybrid"
};
public static string NormalizeSlug(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new InvalidOperationException("Slug must not be empty.");
}
var trimmed = value.Trim().ToLowerInvariant();
var builder = new StringBuilder(trimmed.Length);
var previousWasHyphen = false;
foreach (var raw in trimmed)
{
char c;
if (raw == ' ' || raw == '_' || raw == '-')
{
c = '-';
}
else if (IsAsciiAlphanumeric(raw))
{
c = raw;
}
else
{
throw new InvalidOperationException($"Slug contains unsupported character: '{raw}'.");
}
if (c == '-')
{
if (builder.Length == 0 || previousWasHyphen)
{
continue;
}
builder.Append('-');
previousWasHyphen = true;
}
else
{
builder.Append(c);
previousWasHyphen = false;
}
}
while (builder.Length > 0 && builder[^1] == '-')
{
builder.Length--;
}
if (builder.Length < MinSlugLength || builder.Length > MaxSlugLength)
{
throw new InvalidOperationException(
$"Slug length must be between {MinSlugLength} and {MaxSlugLength} characters.");
}
// The normalization loop guarantees the output matches ^[a-z0-9]+(?:-[a-z0-9]+)*$,
// so no post-loop regex check is required.
return builder.ToString();
}
public static string NormalizeTitle(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new InvalidOperationException("Title must not be empty.");
}
var trimmed = value.Trim();
if (trimmed.Length < MinTitleLength || trimmed.Length > MaxTitleLength)
{
throw new InvalidOperationException(
$"Title length must be between {MinTitleLength} and {MaxTitleLength} characters.");
}
return trimmed;
}
public static string? NormalizeDescription(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
var trimmed = value.Trim();
if (trimmed.Length > MaxDescriptionLength)
{
throw new InvalidOperationException(
$"Description must be at most {MaxDescriptionLength} characters.");
}
return trimmed;
}
public static string NormalizeReviewBody(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new InvalidOperationException("Review body must not be empty.");
}
var trimmed = value.Trim();
if (trimmed.Length < MinReviewBodyLength || trimmed.Length > MaxReviewBodyLength)
{
throw new InvalidOperationException(
$"Review body length must be between {MinReviewBodyLength} and {MaxReviewBodyLength} characters.");
}
return trimmed;
}
public static string? NormalizeFormat(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
var trimmed = value.Trim();
if (!AllowedFormats.Contains(trimmed))
{
throw new InvalidOperationException(
$"Format must be one of: {string.Join(", ", AllowedFormats)}.");
}
return trimmed;
}
private static bool IsAsciiAlphanumeric(char c) =>
(c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
}