pub fn validate(input: &str) -> Result<String, String> {
let name = input.trim().to_ascii_lowercase();
if !name.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-') {
return Err(
"use only lowercase letters, digits, and hyphens — no spaces, dots, or accented/unicode characters"
.to_string(),
);
}
let len = name.chars().count();
if !(3..=32).contains(&len) {
return Err(format!("name must be 3–32 characters (got {len})"));
}
if name.starts_with('-') || name.ends_with('-') {
return Err("name can't start or end with a hyphen".to_string());
}
Ok(name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_valid_labels() {
assert_eq!(validate("alice").unwrap(), "alice");
assert_eq!(validate("foo-bar").unwrap(), "foo-bar");
assert_eq!(validate("a1b2c3").unwrap(), "a1b2c3");
assert_eq!(validate(&"a".repeat(32)).unwrap(), "a".repeat(32));
}
#[test]
fn normalizes_case_and_whitespace_unsurprisingly() {
assert_eq!(validate(" Alice ").unwrap(), "alice");
assert_eq!(validate("MyAgent2").unwrap(), "myagent2");
}
#[test]
fn rejects_unicode_instead_of_silently_mangling() {
assert!(validate("café-shop").is_err());
assert!(validate("日本").is_err());
assert!(validate("über").is_err());
}
#[test]
fn rejects_spaces_and_dots() {
assert!(validate("my cool app").is_err());
assert!(validate("a.b.c").is_err());
assert!(validate("under_score").is_err());
}
#[test]
fn rejects_bad_length() {
assert!(validate("ab").is_err()); assert!(validate(&"a".repeat(33)).is_err()); assert!(validate("").is_err());
}
#[test]
fn rejects_edge_hyphens() {
assert!(validate("-alice").is_err());
assert!(validate("alice-").is_err());
assert!(validate("--").is_err());
}
}