From 921088a22a6cb811d27f264bed43f084aaca6595 Mon Sep 17 00:00:00 2001 From: Overlord Date: Tue, 3 Mar 2026 20:03:29 +0100 Subject: [PATCH] Enhance `PmpEmitter` macro to support additional attributes and default handling --- crates/pmp-emitter/src/lib.rs | 163 +++++++++++++++++++++++----------- 1 file changed, 111 insertions(+), 52 deletions(-) diff --git a/crates/pmp-emitter/src/lib.rs b/crates/pmp-emitter/src/lib.rs index 5b31fee..29633a3 100644 --- a/crates/pmp-emitter/src/lib.rs +++ b/crates/pmp-emitter/src/lib.rs @@ -3,34 +3,53 @@ use proc_macro::TokenStream; use syn::{ Lit, Path, Type, Ident, DeriveInput, parse_macro_input }; use darling::{ ast, Error, Result, FromMeta, FromVariant, FromDeriveInput }; +// ---------------------------------------------------------------------------------------------- // +#[derive(Debug, FromDeriveInput)] +#[darling(forward_attrs(pmp_emitter))] +struct EmitterInput { + ident: Ident, + data: ast::Data, +} + +#[derive(Debug, FromVariant)] +#[darling(forward_attrs(pmp_emitter))] +struct VariantReceiver { + ident: Ident, + fields: ast::Fields, + attrs: Vec, +} + +// ---------------------------------------------------------------------------------------------- // #[derive(Debug, Clone, Copy)] struct StrictBool(bool); -#[derive(Debug, FromDeriveInput)] -#[darling(attributes(PMPEmitter))] -struct EmitterInput { - ident: Ident, - data: ast::Data, - descriptor: String, - exit: StrictBool, +/// - `descriptor = "..."` - prefix shown before all variant messages (default: enum name) +/// - `exit = true|false` - whether to call process::exit (default: false) +#[derive(Debug, FromMeta)] +struct EnumAttrs { + #[darling(default)] + descriptor: Option, + #[darling(default)] + exit: Option, } -#[derive(Debug, FromVariant)] -#[darling(attributes(PMPEmitter))] -struct VariantReceiver { - ident: Ident, - #[allow(unused)] - fields: ast::Fields, - +/// - `message = "..."` - error message (default: variant name as title case) +/// - `colorizer = fn` - optional fn(String) -> String for formatting +/// - `exit = true|false` - overrides enum-level exit +#[derive(Debug, FromMeta)] +struct VariantAttrs { #[darling(default)] exit: Option, - message: String, + #[darling(default)] + message: Option, #[darling(default)] colorizer: Option, } +// ---------------------------------------------------------------------------------------------- // + impl FromMeta for StrictBool { fn from_value(value: &Lit) -> Result @@ -44,6 +63,29 @@ impl FromMeta for StrictBool // intentionally NOT implementing from_word() } +fn parse_pmp_attr(attrs: &[syn::Attribute]) -> T +where + T: Default, +{ + attrs + .iter() + .find(|a| a.path().is_ident("pmp_emitter")) + .map(|a| T::from_meta(&a.meta).expect("invalid pmp_emitter attribute")) + .unwrap_or_default() +} + +impl Default for EnumAttrs { + fn default() -> Self { + Self { descriptor: None, exit: None } + } +} + +impl Default for VariantAttrs { + fn default() -> Self { + Self { message: None, exit: None, colorizer: None } + } +} + // ---------------------------------------------------------------------------------------------- // fn pascal_to_title(s: &str) -> String @@ -61,60 +103,77 @@ fn pascal_to_title(s: &str) -> String // ---------------------------------------------------------------------------------------------- // -#[proc_macro_attribute] -pub fn pmp_emitter(_attr: TokenStream, item: TokenStream) -> TokenStream +#[proc_macro_derive(PmpEmitter, attributes(pmp_emitter))] +pub fn pmp_emitter(item: TokenStream) -> TokenStream { let input = parse_macro_input!(item as DeriveInput); - let args = match EmitterInput::from_derive_input(&input) { + + // parse enum shape + let shape = match EmitterInput::from_derive_input(&input) { Ok(a) => a, Err(e) => return e.write_errors().into(), }; - let enum_ident = &args.ident; - let descriptor = &args.descriptor; - let global_exit = args.exit.0; + // parse enum-level #[pmp_emitter(...)] directly from the raw attrs + let enum_attrs: EnumAttrs = parse_pmp_attr(&input.attrs); - let variants = args + let enum_ident = &shape.ident; + let descriptor = enum_attrs.descriptor + .unwrap_or_else(|| pascal_to_title(&enum_ident.to_string())); + let global_exit = enum_attrs.exit.map(|b| b.0).unwrap_or(false); + + let variants = shape .data .take_enum() - .expect("pmp_emitter can only be applied to enums"); + .expect("PmpEmitter can only be derived on enums"); - let match_arms = variants.iter().map(|v| - { - let var_ident = &v.ident; - let message = &v.message; - let should_exit = v.exit.map(|b| b.0).unwrap_or(global_exit); - let title = pascal_to_title(&var_ident.to_string()); - - // ┌─ Output format ───────────────────────────────┐ - // │ {descriptor}: {Variant Title} ("{message}") │ - // │ │ - // │ {stacktrace} │ - // └───────────────────────────────────────────────┘ - let header = format!("{}: {} (\"{}\")", descriptor, title, message); - - // Apply colorizer fn if provided, otherwise plain string - let formatted_header = match &v.colorizer + let match_arms = variants.iter().map(|v| { - Some(path) => quote! { #path(#header) }, - None => quote! { #header.to_string() }, - }; + let var_ident = &v.ident; + let title = pascal_to_title(&var_ident.to_string()); - let exit_tokens = if should_exit { quote! { std::process::exit(1); } } else { quote! {} }; + // parse variant-level #[pmp_emitter(...)] from the variant's raw attrs + let vattrs: VariantAttrs = parse_pmp_attr(&v.attrs); - quote! { - #enum_ident::#var_ident(trace) => + let message = vattrs.message.unwrap_or_else(|| title.clone()); + let should_exit = vattrs.exit.map(|b| b.0).unwrap_or(global_exit); + + // ┌─ Output format ───────────────────────────────┐ + // │ {descriptor}: {Variant Title} ("{message}") │ + // │ │ + // │ {stacktrace} │ + // └───────────────────────────────────────────────┘ + let header = format!("{}: {} (\"{}\")", descriptor, title, message); + + let formatted_header = match &vattrs.colorizer { - println!("{}\n\n{}", #formatted_header, trace); - #exit_tokens + Some(path) => quote! { #path(#header) }, + None => quote! { #header.to_string() }, + }; + + let exit_tokens = if should_exit { quote! { std::process::exit(1); } } else { quote! {} }; + + match v.fields.style + { + ast::Style::Tuple => quote! { + #enum_ident::#var_ident(trace) => + { + println!("{}\n\n{}", #formatted_header, trace); + #exit_tokens + } + }, + _ => quote! { + #enum_ident::#var_ident => + { + println!("{}", #formatted_header); + #exit_tokens + } + }, } - } - }); + }); quote! { - #input // re-emit the original enum untouched - - impl #enum_ident + impl #enum_ident { pub fn emit(&self) { match self {