feat: add to_snake_case_string proc macro implementation

Co-authored-by: aider (openai/andrew/openrouter/deepseek/deepseek-chat-v3.1) <aider@aider.chat>
This commit is contained in:
Andrew Phillips
2025-08-27 12:07:16 -03:00
parent 2f0e7e1c5e
commit 6579d47821
2 changed files with 97 additions and 0 deletions

12
macros/Cargo.toml Normal file
View File

@@ -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"

View File

@@ -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
}