pub(crate) fn to_const_ident(raw: &str) -> String {
let mut out = String::with_capacity(raw.len() + 1);
for ch in raw.chars() {
if ch.is_ascii_alphanumeric() {
out.push(ch.to_ascii_uppercase());
} else {
out.push('_');
}
}
if out.is_empty() {
out.push('_');
}
if out
.chars()
.next()
.is_some_and(|first| first.is_ascii_digit())
{
out.insert(0, '_');
}
out
}
pub(crate) fn validate_rust_path(path: &str, flag: &str) -> Result<(), String> {
if path.is_empty() {
return Err(format!("{flag}: path must be non-empty `ident(::ident)*`"));
}
for segment in path.split("::") {
if !is_rust_ident(segment) {
return Err(format!(
"{flag}: invalid path `{path}` (expected `ident(::ident)*`)"
));
}
}
Ok(())
}
fn is_rust_ident(value: &str) -> bool {
let mut chars = value.chars();
let Some(first) = chars.next() else {
return false;
};
if !(first.is_ascii_alphabetic() || first == '_') {
return false;
}
chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn const_ident_normalizes_name() {
assert_eq!(to_const_ident("short-click"), "SHORT_CLICK");
assert_eq!(to_const_ident("9v"), "_9V");
}
#[test]
fn rust_path_accepts_ident_chains() {
validate_rust_path("crate::curves", "--curves-module").unwrap();
validate_rust_path("ph_haptics", "--haptics-crate").unwrap();
validate_rust_path("a::b::c", "--curves-module").unwrap();
}
#[test]
fn rust_path_rejects_invalid_forms() {
assert!(validate_rust_path("", "--curves-module").is_err());
assert!(validate_rust_path("crate/curves", "--curves-module").is_err());
assert!(validate_rust_path("::crate", "--curves-module").is_err());
assert!(validate_rust_path("crate::", "--curves-module").is_err());
assert!(validate_rust_path("1bad", "--haptics-crate").is_err());
assert!(validate_rust_path("foo-bar", "--haptics-crate").is_err());
}
}