summaryrefslogtreecommitdiff
path: root/rola-utils/macros/src/constants.rs
blob: e5fe668fd27b4efdbd1ba79a30e66bb9a90b9780 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
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 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<str> }
            })
            .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<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
}