From cbdc635cb32e7e9790172eee75e50549e61aa24d Mon Sep 17 00:00:00 2001 From: Overlord Date: Tue, 3 Mar 2026 19:05:15 +0100 Subject: [PATCH] Add `pmp-emitter` crate with proc-macro for enum-based emitters (v 1.0.0) --- crates/pmp-emitter/Cargo.toml | 16 +++++ crates/pmp-emitter/src/lib.rs | 126 ++++++++++++++++++++++++++++++++++ src/cli/notifications.rs | 0 3 files changed, 142 insertions(+) create mode 100644 crates/pmp-emitter/Cargo.toml create mode 100644 crates/pmp-emitter/src/lib.rs create mode 100644 src/cli/notifications.rs diff --git a/crates/pmp-emitter/Cargo.toml b/crates/pmp-emitter/Cargo.toml new file mode 100644 index 0000000..cd6de45 --- /dev/null +++ b/crates/pmp-emitter/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "pmp-emitter" +version = "1.0.0" + +edition.workspace = true +authors.workspace = true +license-file.workspace = true + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "^2", features = ["full"] } +quote = "^1" +darling = "^0" +proc-macro2 = "^1" diff --git a/crates/pmp-emitter/src/lib.rs b/crates/pmp-emitter/src/lib.rs new file mode 100644 index 0000000..5b31fee --- /dev/null +++ b/crates/pmp-emitter/src/lib.rs @@ -0,0 +1,126 @@ +use quote::quote; +use proc_macro::TokenStream; +use syn::{ Lit, Path, Type, Ident, DeriveInput, parse_macro_input }; +use darling::{ ast, Error, Result, FromMeta, FromVariant, FromDeriveInput }; + + + +#[derive(Debug, Clone, Copy)] +struct StrictBool(bool); + +#[derive(Debug, FromDeriveInput)] +#[darling(attributes(PMPEmitter))] +struct EmitterInput { + ident: Ident, + data: ast::Data, + descriptor: String, + exit: StrictBool, +} + +#[derive(Debug, FromVariant)] +#[darling(attributes(PMPEmitter))] +struct VariantReceiver { + ident: Ident, + #[allow(unused)] + fields: ast::Fields, + + #[darling(default)] + exit: Option, + message: String, + #[darling(default)] + colorizer: Option, +} + +impl FromMeta for StrictBool +{ + fn from_value(value: &Lit) -> Result + { + match value { + Lit::Bool(b) => Ok(StrictBool(b.value)), + _ => Err(Error::unexpected_lit_type(value)), + } + } + + // intentionally NOT implementing from_word() +} + +// ---------------------------------------------------------------------------------------------- // + +fn pascal_to_title(s: &str) -> String +{ + let mut out = String::new(); + + for (i, ch) in s.chars().enumerate() + { + if ch.is_uppercase() && i > 0 { out.push(' '); } + out.push(ch); + } + + out +} + +// ---------------------------------------------------------------------------------------------- // + +#[proc_macro_attribute] +pub fn pmp_emitter(_attr: TokenStream, item: TokenStream) -> TokenStream +{ + let input = parse_macro_input!(item as DeriveInput); + let args = 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; + + let variants = args + .data + .take_enum() + .expect("pmp_emitter can only be applied to 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 + { + Some(path) => quote! { #path(#header) }, + None => quote! { #header.to_string() }, + }; + + let exit_tokens = if should_exit { quote! { std::process::exit(1); } } else { quote! {} }; + + quote! { + #enum_ident::#var_ident(trace) => + { + println!("{}\n\n{}", #formatted_header, trace); + #exit_tokens + } + } + }); + + quote! { + #input // re-emit the original enum untouched + + impl #enum_ident + { + pub fn emit(&self) { + match self { + #(#match_arms),* + } + } + } + }.into() +} diff --git a/src/cli/notifications.rs b/src/cli/notifications.rs new file mode 100644 index 0000000..e69de29