Add pmp-emitter crate with proc-macro for enum-based emitters (v 1.0.0)
This commit is contained in:
16
crates/pmp-emitter/Cargo.toml
Normal file
16
crates/pmp-emitter/Cargo.toml
Normal file
@@ -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"
|
||||||
126
crates/pmp-emitter/src/lib.rs
Normal file
126
crates/pmp-emitter/src/lib.rs
Normal file
@@ -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<VariantReceiver, ()>,
|
||||||
|
descriptor: String,
|
||||||
|
exit: StrictBool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, FromVariant)]
|
||||||
|
#[darling(attributes(PMPEmitter))]
|
||||||
|
struct VariantReceiver {
|
||||||
|
ident: Ident,
|
||||||
|
#[allow(unused)]
|
||||||
|
fields: ast::Fields<Type>,
|
||||||
|
|
||||||
|
#[darling(default)]
|
||||||
|
exit: Option<StrictBool>,
|
||||||
|
message: String,
|
||||||
|
#[darling(default)]
|
||||||
|
colorizer: Option<Path>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromMeta for StrictBool
|
||||||
|
{
|
||||||
|
fn from_value(value: &Lit) -> Result<Self>
|
||||||
|
{
|
||||||
|
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()
|
||||||
|
}
|
||||||
0
src/cli/notifications.rs
Normal file
0
src/cli/notifications.rs
Normal file
Reference in New Issue
Block a user