dterror-derive 0.1.0

Derive macro for the dterror FromContext trait
Documentation
//! Identifier case conversion for generated constructor names.

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

/// Convert a variant name to its `snake_case` form as an [`Ident`].
///
/// The conversion itself is delegated to [`heck::ToSnekCase`]. A converted name does not
/// always parse as a bare identifier: a variant named `Type` converts to the keyword `type`,
/// and an underscore-only name converts to the empty string. The result is therefore parsed
/// with [`syn::parse_str`], retrying as a raw identifier (`r#type`) before falling back to
/// the original identifier.
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() {
        // `to_string` 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(), "__");
    }
}