use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields};
fn snake_to_title_case(s: &str) -> String {
s.split('_')
.filter(|part| !part.is_empty())
.map(|part| {
let mut chars = part.chars();
match chars.next() {
Some(first) => {
let upper: String = first.to_uppercase().collect();
format!("{}{}", upper, chars.as_str())
}
None => String::new(),
}
})
.collect::<Vec<_>>()
.join(" ")
}
#[proc_macro_derive(Table)]
pub fn derive_table(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let struct_name = &input.ident;
let struct_name_str = struct_name.to_string();
let fields = match &input.data {
Data::Struct(data_struct) => match &data_struct.fields {
Fields::Named(named) => &named.named,
Fields::Unnamed(_) => {
return syn::Error::new_spanned(
struct_name,
"Table derive only supports structs with named fields",
)
.to_compile_error()
.into();
}
Fields::Unit => {
return syn::Error::new_spanned(
struct_name,
"Table derive does not support unit structs",
)
.to_compile_error()
.into();
}
},
Data::Enum(_) => {
return syn::Error::new_spanned(struct_name, "Table derive does not support enums")
.to_compile_error()
.into();
}
Data::Union(_) => {
return syn::Error::new_spanned(struct_name, "Table derive does not support unions")
.to_compile_error()
.into();
}
};
let headers: Vec<String> = fields
.iter()
.map(|f| {
let name = f.ident.as_ref().expect("named field must have ident");
snake_to_title_case(&name.to_string())
})
.collect();
let field_idents: Vec<_> = fields
.iter()
.map(|f| f.ident.as_ref().expect("named field must have ident"))
.collect();
let header_literals: Vec<_> = headers.iter().map(|h| h.as_str()).collect::<Vec<_>>();
let header_tokens = header_literals.iter().map(|h| quote! { #h });
let row_expr = field_idents.iter().map(|ident| {
quote! { &item.#ident.to_string() }
});
let expanded = quote! {
impl #struct_name {
pub fn to_table(items: &[Self]) -> gilt::table::Table {
let mut table = gilt::table::Table::new(&[#(#header_tokens),*]);
table.title = Some(#struct_name_str.to_string());
for item in items {
table.add_row(&[#(#row_expr),*]);
}
table
}
}
};
TokenStream::from(expanded)
}
#[cfg(test)]
mod tests {
use super::snake_to_title_case;
#[test]
fn test_snake_to_title_case() {
assert_eq!(snake_to_title_case("first_name"), "First Name");
assert_eq!(snake_to_title_case("age"), "Age");
assert_eq!(snake_to_title_case("department_id"), "Department Id");
assert_eq!(snake_to_title_case("a_b_c"), "A B C");
assert_eq!(snake_to_title_case("single"), "Single");
}
#[test]
fn test_snake_to_title_case_edge_cases() {
assert_eq!(snake_to_title_case(""), "");
assert_eq!(snake_to_title_case("_leading"), "Leading");
assert_eq!(snake_to_title_case("trailing_"), "Trailing");
}
}