diff --git a/macros/Cargo.toml b/macros/Cargo.toml new file mode 100644 index 0000000..0763340 --- /dev/null +++ b/macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "to_snake_case_string" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2.0", features = ["full"] } +quote = "1.0" +proc-macro2 = "1.0" diff --git a/macros/src/lib.rs b/macros/src/lib.rs index e69de29..dbe17ea 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -0,0 +1,85 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, Data, DeriveInput}; + +// First, define the trait that we want to implement. +// This could also live in a separate, non-proc-macro crate if you wanted. +pub trait ToSnakeCaseString { + fn to_snake_case(&self) -> String; +} + +/// This is the derive macro function. +/// The `#[proc_macro_derive(ToSnakeCaseString)]` attribute tells the compiler +/// that this function implements the derive macro for the `ToSnakeCaseString` trait. +#[proc_macro_derive(ToSnakeCaseString)] +pub fn to_snake_case_string_derive(input: TokenStream) -> TokenStream { + // Parse the input tokens into a syntax tree + let ast = parse_macro_input!(input as DeriveInput); + + // Get the name of the enum we're deriving for + let name = &ast.ident; + + // Ensure we're working with an enum + let data_enum = match ast.data { + Data::Enum(data) => data, + _ => panic!("ToSnakeCaseString can only be derived for enums"), + }; + + // Iterate over the enum variants and generate a match arm for each one + let match_arms = data_enum.variants.iter().map(|variant| { + let variant_ident = &variant.ident; + // Convert the PascalCase variant name to snake_case for the string literal + let snake_case_string = to_snake_case(variant_ident.to_string()); + + // Generate the code for a single match arm, e.g., + // `Self::MyVariant => "my_variant".to_string(),` + // We ignore variant fields (e.g., `MyVariant(u32)`) by using `..` + quote! { + #name::#variant_ident { .. } => #snake_case_string.to_string(), + } + }); + + // Generate the full implementation of the trait for the enum + let gen = quote! { + // This is the implementation of our custom trait + impl ToSnakeCaseString for #name { + // This is the implementation of the trait's method + fn to_snake_case(&self) -> String { + match self { + // Expand the generated match arms inside the match block + #( #match_arms )* + } + } + } + + // We can also optionally implement the standard `ToString` or `Display` trait + impl std::string::ToString for #name { + fn to_string(&self) -> String { + self.to_snake_case() + } + } + + impl std::fmt::Display for #name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_snake_case()) + } + } + }; + + // Return the generated code as a TokenStream + gen.into() +} + +/// Helper function to convert a PascalCase string to a snake_case string. +fn to_snake_case(pascal_case: String) -> String { + let mut snake = String::new(); + for (i, ch) in pascal_case.chars().enumerate() { + if i > 0 && ch.is_uppercase() { + snake.push('_'); + } + snake.push(ch.to_ascii_lowercase()); + } + snake +}