diff options
Diffstat (limited to 'rola-vcs/internal_macros/src/constants.rs')
| -rw-r--r-- | rola-vcs/internal_macros/src/constants.rs | 115 |
1 files changed, 115 insertions, 0 deletions
diff --git a/rola-vcs/internal_macros/src/constants.rs b/rola-vcs/internal_macros/src/constants.rs new file mode 100644 index 0000000..2e76bfe --- /dev/null +++ b/rola-vcs/internal_macros/src/constants.rs @@ -0,0 +1,115 @@ +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<Item> = 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 function. +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); + + if placeholders.is_empty() { + parse_quote! { + #(#attrs)* + pub fn #name() -> String { + #value_str.to_string() + } + } + } else { + let params: Vec<_> = placeholders + .iter() + .map(|p| { + let ident = format_ident!("{p}"); + quote! { #ident: impl ::core::convert::AsRef<str> } + }) + .collect(); + + let format_args: Vec<_> = placeholders + .iter() + .map(|p| { + let ident = format_ident!("{p}"); + quote! { #ident = #ident.as_ref() } + }) + .collect(); + + parse_quote! { + #(#attrs)* + 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<String> { + 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 +} |
