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 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); 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 Meta::NameValue(name_value) if name_value.path.is_ident("level") => {
187 let _ = attrs.level.insert(name_value.value.try_into()?);
188 }
189 Meta::List(list) if list.path.is_ident("expr") => {
191 let _ = attrs.expr.insert(list.parse_args::<Expr>()?);
192 }
193 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 Meta::Path(path) => {
204 let ident = path.require_ident()?;
205 Ok((Some(ident.clone()), quote!(#ident = #ident)))
206 }
207 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 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}