Enhance PmpEmitter macro to support additional attributes and default handling
This commit is contained in:
@@ -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,
|
#[darling(default)]
|
||||||
|
exit: Option<StrictBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, FromVariant)]
|
/// - `message = "..."` - error message (default: variant name as title case)
|
||||||
#[darling(attributes(PMPEmitter))]
|
/// - `colorizer = fn` - optional fn(String) -> String for formatting
|
||||||
struct VariantReceiver {
|
/// - `exit = true|false` - overrides enum-level exit
|
||||||
ident: Ident,
|
#[derive(Debug, FromMeta)]
|
||||||
#[allow(unused)]
|
struct VariantAttrs {
|
||||||
fields: ast::Fields<Type>,
|
|
||||||
|
|
||||||
#[darling(default)]
|
#[darling(default)]
|
||||||
exit: Option<StrictBool>,
|
exit: Option<StrictBool>,
|
||||||
message: String,
|
#[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,60 +103,77 @@ 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 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) },
|
let var_ident = &v.ident;
|
||||||
None => quote! { #header.to_string() },
|
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! {
|
let message = vattrs.message.unwrap_or_else(|| title.clone());
|
||||||
#enum_ident::#var_ident(trace) =>
|
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);
|
Some(path) => quote! { #path(#header) },
|
||||||
#exit_tokens
|
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! {
|
quote! {
|
||||||
#input // re-emit the original enum untouched
|
impl #enum_ident
|
||||||
|
|
||||||
impl #enum_ident
|
|
||||||
{
|
{
|
||||||
pub fn emit(&self) {
|
pub fn emit(&self) {
|
||||||
match self {
|
match self {
|
||||||
|
|||||||
Reference in New Issue
Block a user