#[must_use]
pub fn normalize(item: &str) -> String {
item.chars()
.filter(|&c| c != '-' && c != '_')
.flat_map(char::to_lowercase)
.collect()
}
#[must_use]
pub fn canonical<'a>(candidate: &str, declared: &[&'a str]) -> Option<&'a str> {
if let Some(exact) = declared.iter().find(|d| **d == candidate) {
return Some(exact);
}
let wanted = normalize(candidate);
let mut hits = declared.iter().filter(|d| normalize(d) == wanted);
let first = *hits.next()?;
hits.next().is_none().then_some(first)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn separators_and_case_are_noise() {
assert_eq!(normalize("Max-Connections"), "maxconnections");
assert_eq!(normalize("max_connections"), "maxconnections");
assert_eq!(normalize("MAXCONNECTIONS"), "maxconnections");
}
#[test]
fn exact_match_wins() {
assert_eq!(
canonical("max-conn", &["max-conn", "maxconn"]),
Some("max-conn")
);
}
#[test]
fn spelling_is_recovered() {
assert_eq!(
canonical("Raw_Binary", &["hex", "raw-binary"]),
Some("raw-binary")
);
}
#[test]
fn ambiguity_and_strangers_are_left_alone() {
assert_eq!(canonical("maxconn", &["max-conn", "max_conn"]), None);
assert_eq!(canonical("nonsense", &["hex", "raw-binary"]), None);
}
}