dingtalk_stream_sdk_rust_macro/
lib.rs

1use proc_macro2::{Ident, Span, TokenStream, TokenTree};
2use quote::quote;
3use syn::Error;
4
5/// Proc-macro to make construct SampleActionCard more convernient.
6/// # Example
7///
8/// ```rust
9/// // accept a const tuple as action button
10/// const ACTION_BTN: (&str, &str) = ("btn_title", "btn_url");
11///
12/// // also accept function return tuple
13/// fn DYNAMIC_BTN(p: &str) -> (&'static str, String) {
14///     ("btn_title", format!("btn_url_dynamic_{p}"))
15/// }
16///
17/// action_card! {
18///     "Card Title",
19///     format!("card text"),
20///     [ACTION_BTN, (DYNAMIC_BTN("something"))]
21/// }
22/// ```
23#[proc_macro]
24pub fn action_card(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
25    try_expand(input, parse)
26}
27
28fn try_expand<P>(input: proc_macro::TokenStream, proc: P) -> proc_macro::TokenStream
29where
30    P: FnOnce(TokenStream) -> Result<TokenStream, Error>,
31{
32    match proc(TokenStream::from(input)) {
33        Ok(tokens) => tokens,
34        Err(err) => err.to_compile_error(),
35    }
36    .into()
37}
38
39fn parse(input: TokenStream) -> Result<TokenStream, Error> {
40    let mut input = input.into_iter().peekable();
41    let title = next(&mut input)?;
42    let text = next(&mut input)?;
43
44    let mut count = 0;
45    let mut actions_expanded = Vec::new();
46    let actions = next(&mut input)?;
47    let TokenTree::Group(group) = actions
48        .into_iter()
49        .next()
50        .ok_or_else(|| Error::new(Span::call_site(), "actions is empty"))?
51    else {
52        return Err(Error::new(Span::call_site(), "actions should inside []"));
53    };
54
55    for item in group.stream() {
56        match item {
57            TokenTree::Ident(..) | TokenTree::Group(..) => {
58                count += 1;
59                let title_key = Ident::new(&format!("action_title_{count}"), Span::call_site());
60                let url_key = Ident::new(&format!("action_url_{count}"), Span::call_site());
61
62                if let TokenTree::Ident(i) = item {
63                    actions_expanded.push(quote! {
64                        #title_key: #i.0.to_owned(),
65                        #url_key: #i.1.to_owned(),
66                    });
67                } else if let TokenTree::Group(g) = item {
68                    let tokens = g.stream();
69                    actions_expanded.push(quote! {
70                        #title_key: #tokens.0.to_owned(),
71                        #url_key: #tokens.1.to_owned(),
72                    });
73                }
74            }
75            _ => {}
76        }
77    }
78
79    let enum_name = Ident::new(&format!("SampleActionCard{count}"), Span::call_site());
80    let quote = quote! {
81        MessageTemplate::#enum_name {
82            title: #(#title)*.to_owned(),
83            text: #(#text)*.to_owned(),
84            #(#actions_expanded)*
85        }
86    };
87
88    Ok(quote)
89}
90
91fn next<I: Iterator<Item = TokenTree>>(i: &mut I) -> Result<Vec<TokenTree>, Error> {
92    let mut result = vec![];
93    loop {
94        let Some(n) = i.next() else {
95            break;
96        };
97
98        if let TokenTree::Punct(ref p) = n {
99            if p.as_char() == ',' {
100                break;
101            }
102        }
103
104        result.push(n);
105    }
106
107    Ok(result)
108}