pub fn validate_component(component: &str) -> Result<(), String> {
if component.is_empty() {
return Err("empty component".to_string());
}
if component.chars().all(|c| c.is_ascii_digit()) {
return Ok(());
}
let mut chars = component.chars();
let first = chars.next().unwrap();
let valid_start = unicode_ident::is_xid_start(first)
|| (first == '_'
&& chars
.clone()
.next()
.is_some_and(unicode_ident::is_xid_continue));
if !valid_start {
return Err("must start with a letter or underscore followed by letter/digit".to_string());
}
for c in chars {
if !unicode_ident::is_xid_continue(c) {
return Err(format!("invalid character '{}' in identifier", c));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_components() {
for s in ["foo", "_foo", "café", "名前", "0", "42", "a1", "_1"] {
assert!(validate_component(s).is_ok(), "expected valid: {s:?}");
}
}
#[test]
fn invalid_components() {
for s in ["", "_", "-", "a-b", "a b", ".hidden", "1abc", "a/b", "a$b"] {
assert!(validate_component(s).is_err(), "expected invalid: {s:?}");
}
}
}