use proc_macro::TokenStream; use quote::{format_ident, quote}; use syn::{Expr, Item, ItemConst, ItemMod, Lit, parse_macro_input, parse_quote}; /// Entry point called from lib.rs. pub fn expand(_attr: TokenStream, item: TokenStream) -> TokenStream { let mut input_mod = parse_macro_input!(item as ItemMod); let (_, items) = match &mut input_mod.content { Some(content) => content, None => panic!("#[constants] can only be applied to a module with a body"), }; let mut new_items: Vec = Vec::with_capacity(items.len()); for item in items.iter() { if let Item::Const(const_item) = item { let func = transform_const(const_item); new_items.push(func); } else { new_items.push(item.clone()); } } let mod_ident = &input_mod.ident; let vis = &input_mod.vis; let output = quote! { #[allow(non_snake_case)] #vis mod #mod_ident { #(#new_items)* } }; output.into() } /// Transforms a single `const` item into a functionif. fn transform_const(const_item: &ItemConst) -> Item { let name = &const_item.ident; let attrs = &const_item.attrs; // Extract the string literal value from the const let value_str = match &*const_item.expr { Expr::Lit(expr_lit) => match &expr_lit.lit { Lit::Str(lit_str) => lit_str.value(), _ => panic!( "#[constants] only supports `&str` literals, \ but `{name}` has a non-string literal" ), }, _ => panic!( "#[constants] only supports literal expressions, \ but `{name}` has a non-literal expression" ), }; let placeholders = extract_placeholders(&value_str); // Build a doc comment that shows the original constant value let doc_comment = format!( "Generated from const `{}` with value: \"{}\"", name, value_str.replace('\"', "\\\"") ); if placeholders.is_empty() { parse_quote! { #(#attrs)* #[doc = #doc_comment] pub const #name: &'static str = #value_str; } } else { let params: Vec<_> = placeholders .iter() .map(|p| { let ident = format_ident!("{p}"); quote! { #ident: impl ::core::convert::AsRef } }) .collect(); let format_args: Vec<_> = placeholders .iter() .map(|p| { let ident = format_ident!("{p}"); quote! { #ident = #ident.as_ref() } }) .collect(); parse_quote! { #(#attrs)* #[doc = #doc_comment] pub fn #name(#(#params),*) -> String { ::std::format!(#value_str, #(#format_args),*) } } } } /// Extracts all `{name}` placeholder identifiers from a format string. fn extract_placeholders(s: &str) -> Vec { let mut placeholders = Vec::new(); let mut chars = s.char_indices().peekable(); while let Some((_, c)) = chars.next() { if c == '{' { let mut name = String::new(); for (_, c) in &mut chars { if c == '}' { break; } name.push(c); } let trimmed = name.trim().to_string(); if !trimmed.is_empty() { placeholders.push(trimmed); } } } placeholders }