openmina_macros/
action_event.rs

1use std::convert::TryInto;
2
3use proc_macro2::*;
4use quote::*;
5use syn::*;
6
7#[derive(Clone, Debug)]
8enum Level {
9    Error,
10    Warn,
11    Info,
12    Debug,
13    Trace,
14}
15
16impl TryFrom<Expr> for Level {
17    type Error = Error;
18
19    fn try_from(value: Expr) -> Result<Level> {
20        let Expr::Path(ExprPath { path, .. }) = value else {
21            return Err(Error::new_spanned(value, "ident is expected"));
22        };
23        let level = match path.require_ident()?.to_string().to_lowercase().as_str() {
24            "error" => Level::Error,
25            "warn" => Level::Warn,
26            "info" => Level::Info,
27            "debug" => Level::Debug,
28            "trace" => Level::Trace,
29            _ => return Err(Error::new_spanned(path, "incorrect value")),
30        };
31        Ok(level)
32    }
33}
34
35impl ToTokens for Level {
36    fn to_tokens(&self, tokens: &mut TokenStream) {
37        let ident = match self {
38            Level::Error => format_ident!("action_error"),
39            Level::Warn => format_ident!("action_warn"),
40            Level::Info => format_ident!("action_info"),
41            Level::Debug => format_ident!("action_debug"),
42            Level::Trace => format_ident!("action_trace"),
43        };
44        tokens.extend(quote!(#ident));
45    }
46}
47
48#[derive(Clone, Debug)]
49enum FieldsSpec {
50    /// List of expressions for fields to be added to the tracing, with
51    /// optional ident for filtering.
52    Some(Vec<(Option<Ident>, TokenStream)>),
53}
54
55#[derive(Clone, Debug, Default)]
56struct ActionEventAttrs {
57    level: Option<Level>,
58    fields: Option<FieldsSpec>,
59    expr: Option<Expr>,
60}
61
62pub fn expand(input: DeriveInput) -> Result<TokenStream> {
63    let Data::Enum(enum_data) = &input.data else {
64        return Err(Error::new_spanned(input, "should be enum"));
65    };
66    let type_name = &input.ident;
67    let trait_name = quote!(openmina_core::ActionEvent); // TODO
68    let input_attrs = action_event_attrs(&input.attrs)?;
69    let variants = enum_data
70        .variants
71        .iter()
72        .map(|v| {
73            let variant_name = &v.ident;
74            let mut args = vec![quote!(context)];
75            let variant_attrs = action_event_attrs(&v.attrs)?;
76            match &v.fields {
77                Fields::Unnamed(fields) => {
78                    if fields.unnamed.len() != 1 {
79                        return Err(Error::new_spanned(
80                            fields,
81                            "only single-item variant supported",
82                        ));
83                    }
84                    if fields.unnamed.len() != 1 {
85                        return Err(Error::new_spanned(
86                            fields,
87                            "only single-item variant supported",
88                        ));
89                    }
90                    Ok(quote! {
91                        #type_name :: #variant_name (action) => action.action_event(#(#args),*),
92                    })
93                }
94                Fields::Named(fields_named) => {
95                    let field_names = fields_named.named.iter().map(|named| &named.ident);
96                    if let Some(expr) = variant_attrs.expr {
97                        return Ok(quote! {
98                            #type_name :: #variant_name { #(#field_names),* } => #expr,
99                        });
100                    }
101                    args.push(kind_field(type_name, &v.ident)?);
102                    args.extend(summary_field(&v.attrs)?);
103                    args.extend(fields(&variant_attrs.fields, &input_attrs.fields, fields_named)?);
104                    let level = level(&variant_attrs.level, &v.ident, &input_attrs.level);
105                    Ok(quote! {
106                        #type_name :: #variant_name { #(#field_names),* } => openmina_core::#level!(#(#args),*),
107                    })
108                }
109                Fields::Unit => {
110                    if let Some(expr) = variant_attrs.expr {
111                        return Ok(quote! {
112                            #type_name :: #variant_name => #expr,
113                        });
114                    }
115                    args.push(kind_field(type_name, &v.ident)?);
116                    args.extend(summary_field(&v.attrs)?);
117                    let level = level(&variant_attrs.level, &v.ident, &input_attrs.level);
118                    Ok(quote! {
119                        #type_name :: #variant_name => openmina_core::#level!(#(#args),*),
120                    })
121                }
122            }
123        })
124        .collect::<Result<Vec<_>>>()?;
125
126    Ok(quote! {
127        impl #trait_name for #type_name {
128            fn action_event<T>(&self, context: &T)
129            where T: openmina_core::log::EventContext,
130            {
131                #[allow(unused_variables)]
132                match self {
133                    #(#variants)*
134                }
135            }
136        }
137    })
138}
139
140fn level(variant_level: &Option<Level>, variant_name: &Ident, enum_level: &Option<Level>) -> Level {
141    variant_level
142        .as_ref()
143        .cloned()
144        .or_else(|| {
145            let s = variant_name.to_string();
146            (s.ends_with("Error") || s.ends_with("Warn")).then_some(Level::Warn)
147        })
148        .or_else(|| enum_level.as_ref().cloned())
149        .unwrap_or(Level::Debug)
150}
151
152fn fields(
153    variant_fields: &Option<FieldsSpec>,
154    enum_fields: &Option<FieldsSpec>,
155    fields: &FieldsNamed,
156) -> Result<Vec<TokenStream>> {
157    variant_fields
158        .as_ref()
159        .or(enum_fields.as_ref())
160        .map_or_else(|| Ok(Vec::new()), |f| filter_fields(f, fields))
161}
162
163fn filter_fields(field_spec: &FieldsSpec, fields: &FieldsNamed) -> Result<Vec<TokenStream>> {
164    match field_spec {
165        FieldsSpec::Some(f) => f
166            .iter()
167            .filter(|(name, _)| {
168                name.as_ref()
169                    .is_none_or(|name| fields.named.iter().any(|n| Some(name) == n.ident.as_ref()))
170            })
171            .map(|(_, expr)| Ok(expr.clone()))
172            .collect(),
173    }
174}
175
176fn action_event_attrs(attrs: &[Attribute]) -> Result<ActionEventAttrs> {
177    attrs
178        .iter()
179        .filter(|attr| attr.path().is_ident("action_event"))
180        .try_fold(ActionEventAttrs::default(), |mut attrs, attr| {
181            let nested =
182                attr.parse_args_with(punctuated::Punctuated::<Meta, Token![,]>::parse_terminated)?;
183            nested.into_iter().try_for_each(|meta| {
184                match meta {
185                    // #[level = ...]
186                    Meta::NameValue(name_value) if name_value.path.is_ident("level") => {
187                        let _ = attrs.level.insert(name_value.value.try_into()?);
188                    }
189                    // #[expr(...)]
190                    Meta::List(list) if list.path.is_ident("expr") => {
191                        let _ = attrs.expr.insert(list.parse_args::<Expr>()?);
192                    }
193                    // #[fields(...)]
194                    Meta::List(list) if list.path.is_ident("fields") => {
195                        let nested = list.parse_args_with(
196                            punctuated::Punctuated::<Meta, Token![,]>::parse_terminated,
197                        )?;
198                        let fields = nested
199                            .iter()
200                            .map(|meta| {
201                                match meta {
202                                    // field
203                                    Meta::Path(path) => {
204                                        let ident = path.require_ident()?;
205                                        Ok((Some(ident.clone()), quote!(#ident = #ident)))
206                                    }
207                                    // field = expr
208                                    Meta::NameValue(name_value) => {
209                                        let event_field = name_value.path.require_ident()?;
210                                        let expr = &name_value.value;
211                                        let maybe_field = get_field_name(expr);
212                                        Ok((maybe_field.cloned(), quote!(#event_field = #expr)))
213                                    }
214                                    // debug(field)
215                                    // display(field)
216                                    Meta::List(list)
217                                        if list.path.is_ident("debug")
218                                            || list.path.is_ident("display") =>
219                                    {
220                                        let conv = list.path.require_ident()?;
221                                        let Expr::Path(field) = list.parse_args::<Expr>()? else {
222                                            return Err(Error::new_spanned(
223                                                list,
224                                                "identifier is expected",
225                                            ));
226                                        };
227                                        let field = field.path.require_ident()?;
228                                        Ok((Some(field.clone()), quote!(#field = #conv(#field))))
229                                    }
230                                    _ => Err(Error::new_spanned(meta, "unrecognized repr")),
231                                }
232                            })
233                            .collect::<Result<Vec<_>>>()?;
234                        let _ = attrs.fields.insert(FieldsSpec::Some(fields));
235                    }
236                    _ => return Err(Error::new_spanned(meta, "unrecognized repr")),
237                }
238                Ok(())
239            })?;
240            Ok(attrs)
241        })
242}
243
244fn get_field_name(expr: &Expr) -> Option<&Ident> {
245    match expr {
246        Expr::Path(path) => path.path.require_ident().ok(),
247        Expr::Field(field) => get_field_name(&field.base),
248        Expr::Reference(reference) => get_field_name(&reference.expr),
249        Expr::Unary(ExprUnary { expr, .. }) => get_field_name(expr),
250        Expr::Call(call) => match call.func.as_ref() {
251            Expr::Path(path) if path.path.is_ident("display") || path.path.is_ident("debug") => {
252                call.args.first().and_then(|arg| get_field_name(arg))
253            }
254            Expr::Field(field) => get_field_name(&field.base),
255            _ => None,
256        },
257        _ => None,
258    }
259}
260
261fn kind_field(enum_name: &Ident, variant_name: &Ident) -> Result<TokenStream> {
262    let enum_name = enum_name.to_string();
263    let first = enum_name.strip_suffix("Action").unwrap_or(&enum_name);
264    let second = variant_name.to_string();
265    let kind = format!("{first}{second}");
266    Ok(quote!(kind = #kind))
267}
268
269fn summary_field(attrs: &[Attribute]) -> Result<Option<TokenStream>> {
270    let Some(doc_attr) = attrs.iter().find(|attr| attr.path().is_ident("doc")) else {
271        return Ok(None);
272    };
273    let name_value = doc_attr.meta.require_name_value()?;
274    let Expr::Lit(ExprLit {
275        lit: Lit::Str(lit), ..
276    }) = &name_value.value
277    else {
278        return Ok(None);
279    };
280    let value = lit.value();
281    let trimmed = value.trim();
282    let stripped = trimmed.strip_suffix('.').unwrap_or(trimmed);
283    Ok(Some(quote!(summary = #stripped)))
284}
285
286#[cfg(test)]
287mod tests {
288    use rust_format::{Formatter, RustFmt};
289
290    fn test(input: &str, expected: &str) -> anyhow::Result<()> {
291        let fmt = RustFmt::default();
292
293        let expected = fmt.format_str(expected)?;
294        let input = syn::parse_str::<syn::DeriveInput>(input)?;
295        let output = super::expand(input)?;
296        let output = fmt.format_tokens(output)?;
297        assert_eq!(
298            output, expected,
299            "\n<<<<<<\n{}======\n{}>>>>>>",
300            output, expected
301        );
302        Ok(())
303    }
304
305    #[test]
306    fn test_delegate() -> anyhow::Result<()> {
307        let input = r#"
308#[derive(openmina_core::ActionEvent)]
309pub enum SuperAction {
310    Sub1(SubAction1),
311    Sub2(SubAction2),
312}
313"#;
314        let expected = r#"
315impl openmina_core::ActionEvent for SuperAction {
316    fn action_event<T>(&self, context: &T)
317    where
318        T: openmina_core::log::EventContext,
319    {
320        #[allow(unused_variables)]
321        match self {
322            SuperAction::Sub1(action) => action.action_event(context),
323            SuperAction::Sub2(action) => action.action_event(context),
324        }
325    }
326}
327"#;
328        test(input, expected)
329    }
330
331    #[test]
332    fn test_unit() -> anyhow::Result<()> {
333        let input = r#"
334#[derive(openmina_core::ActionEvent)]
335pub enum Action {
336    Unit,
337    /// documentation
338    UnitWithDoc,
339    /// Multiline documentation.
340    /// Another line.
341    ///
342    /// And another.
343    UnitWithMultilineDoc,
344}
345"#;
346        let expected = r#"
347impl openmina_core::ActionEvent for Action {
348    fn action_event<T>(&self, context: &T)
349    where
350        T: openmina_core::log::EventContext,
351    {
352        #[allow(unused_variables)]
353        match self {
354            Action::Unit => openmina_core::action_debug!(context),
355            Action::UnitWithDoc => openmina_core::action_debug!(context, summary = "documentation"),
356            Action::UnitWithMultilineDoc => openmina_core::action_debug!(context, summary = "Multiline documentation"),
357        }
358    }
359}
360"#;
361        test(input, expected)
362    }
363
364    #[test]
365    fn test_level() -> anyhow::Result<()> {
366        let input = r#"
367#[derive(openmina_core::ActionEvent)]
368#[action_event(level = trace)]
369pub enum Action {
370    ActionDefaultLevel,
371    #[action_event(level = warn)]
372    ActionOverrideLevel,
373    ActionWithError,
374    ActionWithWarn,
375}
376"#;
377        let expected = r#"
378impl openmina_core::ActionEvent for Action {
379    fn action_event<T>(&self, context: &T)
380    where
381        T: openmina_core::log::EventContext,
382    {
383        #[allow(unused_variables)]
384        match self {
385            Action::ActionDefaultLevel => openmina_core::action_trace!(context),
386            Action::ActionOverrideLevel => openmina_core::action_warn!(context),
387            Action::ActionWithError => openmina_core::action_warn!(context),
388            Action::ActionWithWarn => openmina_core::action_warn!(context),
389        }
390    }
391}
392"#;
393        test(input, expected)
394    }
395
396    #[test]
397    fn test_fields() -> anyhow::Result<()> {
398        let input = r#"
399#[derive(openmina_core::ActionEvent)]
400pub enum Action {
401    NoFields { f1: bool },
402    #[action_event(fields(f1))]
403    Field { f1: bool },
404    #[action_event(fields(f = f1))]
405    FieldWithName { f1: bool },
406    #[action_event(fields(f = f.subfield))]
407    FieldExpr { f: WithSubfield },
408    #[action_event(fields(f = display(f.subfield)))]
409    FieldDisplayExpr { f: WithSubfield },
410    #[action_event(fields(debug(f1)))]
411    DebugField { f1: bool },
412    #[action_event(fields(display(f1)))]
413    DisplayField { f1: bool },
414}
415"#;
416        let expected = r#"
417impl openmina_core::ActionEvent for Action {
418    fn action_event<T>(&self, context: &T)
419    where
420        T: openmina_core::log::EventContext,
421    {
422        #[allow(unused_variables)]
423        match self {
424            Action::NoFields { f1 } => openmina_core::action_debug!(context),
425            Action::Field { f1 } => openmina_core::action_debug!(context, f1 = f1),
426            Action::FieldWithName { f1 } => openmina_core::action_debug!(context, f = f1),
427            Action::FieldExpr { f } => openmina_core::action_debug!(context, f = f.subfield),
428            Action::FieldDisplayExpr { f } => openmina_core::action_debug!(context, f = display(f.subfield)),
429            Action::DebugField { f1 } => openmina_core::action_debug!(context, f1 = debug(f1)),
430            Action::DisplayField { f1 } => openmina_core::action_debug!(context, f1 = display(f1)),
431        }
432    }
433}
434"#;
435        test(input, expected)
436    }
437
438    #[test]
439    fn test_filtered_fields() -> anyhow::Result<()> {
440        let input = r#"
441#[derive(openmina_core::ActionEvent)]
442#[action_event(fields(f1, f2 = f2.sub, f3 = display(f3.sub), f4 = foo()))]
443pub enum Action {
444    Unit,
445    AllFields { f1: bool, f2: WithSub, f3: WithSub },
446    OnlyF1 { f1: bool },
447    WithF3 { f1: bool, f3: WithSub },
448}
449"#;
450        let expected = r#"
451impl openmina_core::ActionEvent for Action {
452    fn action_event<T>(&self, context: &T)
453    where
454        T: openmina_core::log::EventContext,
455    {
456        #[allow(unused_variables)]
457        match self {
458            Action::Unit => openmina_core::action_debug!(context),
459            Action::AllFields { f1, f2, f3 } => openmina_core::action_debug!(context, f1 = f1, f2 = f2.sub, f3 = display(f3.sub), f4 = foo()),
460            Action::OnlyF1 { f1 } => openmina_core::action_debug!(context, f1 = f1, f4 = foo()),
461            Action::WithF3 { f1, f3 } => openmina_core::action_debug!(context, f1 = f1, f3 = display(f3.sub), f4 = foo()),
462        }
463    }
464}
465"#;
466        test(input, expected)
467    }
468
469    #[test]
470    fn test_call() -> anyhow::Result<()> {
471        let input = r#"
472#[derive(openmina_core::ActionEvent)]
473pub enum Action {
474    #[action_event(expr(foo(context)))]
475    Unit,
476    #[action_event(expr(foo(context, f1)))]
477    Named { f1: bool },
478}
479"#;
480        let expected = r#"
481impl openmina_core::ActionEvent for Action {
482    fn action_event<T>(&self, context: &T)
483    where
484        T: openmina_core::log::EventContext,
485    {
486        #[allow(unused_variables)]
487        match self {
488            Action::Unit => foo(context),
489            Action::Named { f1 } => foo(context, f1),
490        }
491    }
492}
493"#;
494        test(input, expected)
495    }
496}