From d20e159731124599c52714b00141d93f5709fddc Mon Sep 17 00:00:00 2001 From: Overlord Date: Wed, 4 Mar 2026 06:41:30 +0100 Subject: [PATCH] Introduce `pmp-emitter-core` crate with `PmpEmitter` proc-macro implementation --- .../crates/pmp-emitter-core/Cargo.toml | 17 ++ .../crates/pmp-emitter-core/src/lib.rs | 190 ++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 crates/pmp-emitter/crates/pmp-emitter-core/Cargo.toml create mode 100644 crates/pmp-emitter/crates/pmp-emitter-core/src/lib.rs diff --git a/crates/pmp-emitter/crates/pmp-emitter-core/Cargo.toml b/crates/pmp-emitter/crates/pmp-emitter-core/Cargo.toml new file mode 100644 index 0000000..9fe2e0c --- /dev/null +++ b/crates/pmp-emitter/crates/pmp-emitter-core/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "pmp-emitter-core" +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/crates/pmp-emitter-core/src/lib.rs b/crates/pmp-emitter/crates/pmp-emitter-core/src/lib.rs new file mode 100644 index 0000000..061c1d5 --- /dev/null +++ b/crates/pmp-emitter/crates/pmp-emitter-core/src/lib.rs @@ -0,0 +1,190 @@ +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, 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); + +/// - `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, +} + +/// - `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, + #[darling(default)] + message: Option, + #[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 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 +{ + 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_derive(PmpEmitter, attributes(pmp_emitter))] +pub fn pmp_emitter(item: TokenStream) -> TokenStream +{ + let input = parse_macro_input!(item as DeriveInput); + + // parse enum shape + let shape = match EmitterInput::from_derive_input(&input) { + Ok(a) => a, + Err(e) => return e.write_errors().into(), + }; + + // parse enum-level #[pmp_emitter(...)] directly from the raw attrs + let enum_attrs: EnumAttrs = parse_pmp_attr(&input.attrs); + + 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("PmpEmitter can only be derived on enums"); + + let match_arms = variants.iter().map(|v| + { + let var_ident = &v.ident; + 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 ───────────────────────────────┐ + // │ {descriptor}: {Variant Title} ("{message}") │ + // │ │ + // │ {stacktrace} │ + // └───────────────────────────────────────────────┘ + let header = format!("{}: {} (\"{}\")", descriptor, title, message); + + let formatted_header = match &vattrs.colorizer + { + 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! { + impl #enum_ident + { + pub fn emit(&self) { + match self { #(#match_arms),* } + } + } + + impl ::pmp_emitter::pmp_emitter_misc::Emittable for #enum_ident + { + fn emit(&self) { + self.emit() + } + } + }.into() +}