use std::fmt;
const MAX_LEN: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum NameError {
Empty,
TooLong(usize),
InvalidChar(char),
EdgeHyphen,
DoubleHyphen,
}
impl fmt::Display for NameError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => f.write_str("a skill name cannot be empty"),
Self::TooLong(len) => {
write!(
f,
"a skill name has at most {MAX_LEN} characters, this one has {len}"
)
}
Self::InvalidChar(c) => {
write!(
f,
"a skill name may only contain a-z, 0-9 and '-', not {c:?}"
)
}
Self::EdgeHyphen => f.write_str("a skill name cannot start or end with '-'"),
Self::DoubleHyphen => f.write_str("a skill name cannot contain '--'"),
}
}
}
impl std::error::Error for NameError {}
pub fn validate_name(name: &str) -> Result<(), NameError> {
if name.is_empty() {
return Err(NameError::Empty);
}
let len = name.chars().count();
if len > MAX_LEN {
return Err(NameError::TooLong(len));
}
if let Some(c) = name
.chars()
.find(|c| !(c.is_ascii_lowercase() || c.is_ascii_digit() || *c == '-'))
{
return Err(NameError::InvalidChar(c));
}
if name.starts_with('-') || name.ends_with('-') {
return Err(NameError::EdgeHyphen);
}
if name.contains("--") {
return Err(NameError::DoubleHyphen);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn names_the_specification_allows_pass() {
let longest = "a".repeat(64);
for name in [
"devserial",
"devknx",
"a",
"pdf-processing",
"x2",
longest.as_str(),
] {
assert_eq!(validate_name(name), Ok(()), "{name}");
}
}
#[test]
fn each_rule_refuses_for_its_own_reason() {
assert_eq!(validate_name(""), Err(NameError::Empty));
assert_eq!(validate_name(&"a".repeat(65)), Err(NameError::TooLong(65)));
assert_eq!(validate_name("DevSerial"), Err(NameError::InvalidChar('D')));
assert_eq!(
validate_name("dev_serial"),
Err(NameError::InvalidChar('_'))
);
assert_eq!(validate_name("-dev"), Err(NameError::EdgeHyphen));
assert_eq!(validate_name("dev-"), Err(NameError::EdgeHyphen));
assert_eq!(validate_name("dev--serial"), Err(NameError::DoubleHyphen));
}
#[test]
fn nothing_that_could_leave_the_directory_passes() {
for name in [
"..", "../x", "a/b", "a\\b", ".hidden", "a.b", "รค", "a b", "a\0b",
] {
assert!(validate_name(name).is_err(), "{name:?} must be refused");
}
}
}