use heck::ToSnekCase;
use proc_macro2::Ident;
pub(crate) fn to_snek_case(ident: &Ident) -> Ident {
let converted = ident.to_string().to_snek_case();
syn::parse_str::<Ident>(&converted)
.or_else(|_| syn::parse_str::<Ident>(&format!("r#{converted}")))
.unwrap_or_else(|_| ident.clone())
}
#[cfg(test)]
mod tests {
use super::*;
use proc_macro2::Span;
fn ident(name: &str) -> Ident {
Ident::new(name, Span::call_site())
}
#[test]
fn single_word_acronym_pair() {
assert_eq!(to_snek_case(&ident("Io")).to_string(), "io");
}
#[test]
fn camel_case_splits_on_case_transitions() {
assert_eq!(to_snek_case(&ident("NotFound")).to_string(), "not_found");
}
#[test]
fn acronym_run_stays_together() {
assert_eq!(to_snek_case(&ident("HTTPError")).to_string(), "http_error");
}
#[test]
fn snake_case_is_idempotent() {
assert_eq!(to_snek_case(&ident("io_error")).to_string(), "io_error");
}
#[test]
fn digits_do_not_force_a_boundary() {
assert_eq!(to_snek_case(&ident("field2")).to_string(), "field2");
}
#[test]
fn keyword_result_becomes_raw_identifier() {
assert_eq!(to_snek_case(&ident("Type")).to_string(), "r#type");
}
#[test]
fn underscore_only_name_falls_back_to_input() {
assert_eq!(to_snek_case(&ident("__")).to_string(), "__");
}
}