dterror-derive 0.2.0

Proc-macros for the dterror CtxError and FromContext traits
Documentation
//! This module converts identifier case for generated constructor names.

use heck::ToSnekCase;
use proc_macro2::Ident;

/// This function converts a variant name to its `snake_case` form as an [`Ident`].
///
/// This function uses [`heck::ToSnekCase`]. A converted name does not always parse
/// as a plain identifier. For example, a variant named `Type` converts to the keyword
/// `type`, and an underscore-only name converts to the empty string. This function
/// parses the result with [`syn::parse_str`]. If the first parse fails, it parses the
/// result again with a raw identifier prefix (for example, `r#type`). If the second parse
/// also fails, it uses the original identifier instead.
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() {
        // The `to_string` method renders raw identifiers with their `r#` prefix.
        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(), "__");
    }
}