Enhance PmpEmitter macro to support additional attributes and default handling

This commit is contained in:
2026-03-03 20:03:29 +01:00
parent cbdc635cb3
commit 921088a22a

View File

@@ -3,34 +3,53 @@ use proc_macro::TokenStream;
use syn::{ Lit, Path, Type, Ident, DeriveInput, parse_macro_input }; use syn::{ Lit, Path, Type, Ident, DeriveInput, parse_macro_input };
use darling::{ ast, Error, Result, FromMeta, FromVariant, FromDeriveInput }; use darling::{ ast, Error, Result, FromMeta, FromVariant, FromDeriveInput };
// ---------------------------------------------------------------------------------------------- //
#[derive(Debug, FromDeriveInput)]
#[darling(forward_attrs(pmp_emitter))]
struct EmitterInput {
ident: Ident,
data: ast::Data<VariantReceiver, ()>,
}
#[derive(Debug, FromVariant)]
#[darling(forward_attrs(pmp_emitter))]
struct VariantReceiver {
ident: Ident,
fields: ast::Fields<Type>,
attrs: Vec<syn::Attribute>,
}
// ---------------------------------------------------------------------------------------------- //
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
struct StrictBool(bool); struct StrictBool(bool);
#[derive(Debug, FromDeriveInput)] /// - `descriptor = "..."` - prefix shown before all variant messages (default: enum name)
#[darling(attributes(PMPEmitter))] /// - `exit = true|false` - whether to call process::exit (default: false)
struct EmitterInput { #[derive(Debug, FromMeta)]
ident: Ident, struct EnumAttrs {
data: ast::Data<VariantReceiver, ()>, #[darling(default)]
descriptor: String, descriptor: Option<String>,
exit: StrictBool,
}
#[derive(Debug, FromVariant)]
#[darling(attributes(PMPEmitter))]
struct VariantReceiver {
ident: Ident,
#[allow(unused)]
fields: ast::Fields<Type>,
#[darling(default)] #[darling(default)]
exit: Option<StrictBool>, exit: Option<StrictBool>,
message: String, }
/// - `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<StrictBool>,
#[darling(default)]
message: Option<String>,
#[darling(default)] #[darling(default)]
colorizer: Option<Path>, colorizer: Option<Path>,
} }
// ---------------------------------------------------------------------------------------------- //
impl FromMeta for StrictBool impl FromMeta for StrictBool
{ {
fn from_value(value: &Lit) -> Result<Self> fn from_value(value: &Lit) -> Result<Self>
@@ -44,6 +63,29 @@ impl FromMeta for StrictBool
// intentionally NOT implementing from_word() // intentionally NOT implementing from_word()
} }
fn parse_pmp_attr<T: FromMeta>(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 fn pascal_to_title(s: &str) -> String
@@ -61,31 +103,41 @@ fn pascal_to_title(s: &str) -> String
// ---------------------------------------------------------------------------------------------- // // ---------------------------------------------------------------------------------------------- //
#[proc_macro_attribute] #[proc_macro_derive(PmpEmitter, attributes(pmp_emitter))]
pub fn pmp_emitter(_attr: TokenStream, item: TokenStream) -> TokenStream pub fn pmp_emitter(item: TokenStream) -> TokenStream
{ {
let input = parse_macro_input!(item as DeriveInput); 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, Ok(a) => a,
Err(e) => return e.write_errors().into(), Err(e) => return e.write_errors().into(),
}; };
let enum_ident = &args.ident; // parse enum-level #[pmp_emitter(...)] directly from the raw attrs
let descriptor = &args.descriptor; let enum_attrs: EnumAttrs = parse_pmp_attr(&input.attrs);
let global_exit = args.exit.0;
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 .data
.take_enum() .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 match_arms = variants.iter().map(|v|
{ {
let var_ident = &v.ident; 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()); let title = pascal_to_title(&var_ident.to_string());
// parse variant-level #[pmp_emitter(...)] from the variant's raw attrs
let vattrs: VariantAttrs = parse_pmp_attr(&v.attrs);
let message = vattrs.message.unwrap_or_else(|| title.clone());
let should_exit = vattrs.exit.map(|b| b.0).unwrap_or(global_exit);
// ┌─ Output format ───────────────────────────────┐ // ┌─ Output format ───────────────────────────────┐
// │ {descriptor}: {Variant Title} ("{message}") │ // │ {descriptor}: {Variant Title} ("{message}") │
// │ │ // │ │
@@ -93,8 +145,7 @@ pub fn pmp_emitter(_attr: TokenStream, item: TokenStream) -> TokenStream
// └───────────────────────────────────────────────┘ // └───────────────────────────────────────────────┘
let header = format!("{}: {} (\"{}\")", descriptor, title, message); let header = format!("{}: {} (\"{}\")", descriptor, title, message);
// Apply colorizer fn if provided, otherwise plain string let formatted_header = match &vattrs.colorizer
let formatted_header = match &v.colorizer
{ {
Some(path) => quote! { #path(#header) }, Some(path) => quote! { #path(#header) },
None => quote! { #header.to_string() }, None => quote! { #header.to_string() },
@@ -102,18 +153,26 @@ pub fn pmp_emitter(_attr: TokenStream, item: TokenStream) -> TokenStream
let exit_tokens = if should_exit { quote! { std::process::exit(1); } } else { quote! {} }; let exit_tokens = if should_exit { quote! { std::process::exit(1); } } else { quote! {} };
quote! { match v.fields.style
{
ast::Style::Tuple => quote! {
#enum_ident::#var_ident(trace) => #enum_ident::#var_ident(trace) =>
{ {
println!("{}\n\n{}", #formatted_header, trace); println!("{}\n\n{}", #formatted_header, trace);
#exit_tokens #exit_tokens
} }
},
_ => quote! {
#enum_ident::#var_ident =>
{
println!("{}", #formatted_header);
#exit_tokens
}
},
} }
}); });
quote! { quote! {
#input // re-emit the original enum untouched
impl #enum_ident impl #enum_ident
{ {
pub fn emit(&self) { pub fn emit(&self) {