Introduce pmp-core crate: provide shared proc-macro utilities, strict attribute parsing, and reusable helpers for pmp-* crates.
This commit is contained in:
12
crates/pmp-core/Cargo.toml
Normal file
12
crates/pmp-core/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "pmp-core"
|
||||||
|
version = "1.0.0"
|
||||||
|
|
||||||
|
edition.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
license-file.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
syn = { version = "^2", features = ["full"] }
|
||||||
|
darling = "^0"
|
||||||
|
proc-macro2 = "^1"
|
||||||
76
crates/pmp-core/src/attrs.rs
Normal file
76
crates/pmp-core/src/attrs.rs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
use darling::{ FromMeta };
|
||||||
|
|
||||||
|
/// Implemented by any item that carries a `Vec<syn::Attribute>`.
|
||||||
|
///
|
||||||
|
/// Allows `parse_pmp_attrs` to be generic over variants, fields, etc.
|
||||||
|
pub trait HasAttrs {
|
||||||
|
fn attrs(&self) -> &[syn::Attribute];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Blanket impl for anything that exposes `.attrs` as a public field via a deref,
|
||||||
|
/// covered by explicit impls below for the common syn types.
|
||||||
|
impl HasAttrs for syn::Field {
|
||||||
|
fn attrs(&self) -> &[syn::Attribute] { &self.attrs }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasAttrs for syn::Variant {
|
||||||
|
fn attrs(&self) -> &[syn::Attribute] { &self.attrs }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------- //
|
||||||
|
|
||||||
|
/// Finds a `#[{attr_name}(...)]` attribute in the given list and parses it into `T`.
|
||||||
|
///
|
||||||
|
/// Returns `Ok(T::default())` if no such attribute is present or
|
||||||
|
/// a span-accurate `Err` if the attribute is present but malformed.
|
||||||
|
///
|
||||||
|
/// Each proc-macro crate passes its own attribute name:
|
||||||
|
/// ```
|
||||||
|
/// // in pmp-emitter-core:
|
||||||
|
/// let attrs: EnumAttrs = parse_pmp_attr(&input.attrs, "pmp_emitter")?;
|
||||||
|
///
|
||||||
|
/// // in pmp-macro-core:
|
||||||
|
/// let attrs: MacroAttrs = parse_pmp_attr(&input.attrs, "pmp_macro")?;
|
||||||
|
/// ```
|
||||||
|
pub fn parse_pmp_attr<T>(attrs: &[syn::Attribute], attr_name: &str) -> syn::Result<T>
|
||||||
|
where
|
||||||
|
T: FromMeta + Default,
|
||||||
|
{
|
||||||
|
match attrs.iter().find(|a| a.path().is_ident(attr_name))
|
||||||
|
{
|
||||||
|
None => Ok(T::default()),
|
||||||
|
Some(attr) => T::from_meta(&attr.meta)
|
||||||
|
.map_err(|e| syn::Error::new_spanned(attr, e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Accumulates errors from parsing `#[{attr_name}(...)]` on a collection of items
|
||||||
|
/// (e.g. enum variants or struct fields) into a single `syn::Error`.
|
||||||
|
///
|
||||||
|
/// Returns `Ok(Vec<T>)` with one parsed value per item, or `Err` containing all
|
||||||
|
/// accumulated errors if any items failed.
|
||||||
|
pub fn parse_pmp_attrs<T, I>(items: I, attr_name: &str) -> syn::Result<Vec<T>>
|
||||||
|
where
|
||||||
|
T: FromMeta + Default,
|
||||||
|
I: IntoIterator<Item: HasAttrs>,
|
||||||
|
{
|
||||||
|
let mut results: Vec<T> = Vec::new();
|
||||||
|
let mut errors: Option<syn::Error> = None;
|
||||||
|
|
||||||
|
for item in items
|
||||||
|
{
|
||||||
|
match parse_pmp_attr(item.attrs(), attr_name)
|
||||||
|
{
|
||||||
|
Ok(v) => results.push(v),
|
||||||
|
Err(e) => match &mut errors {
|
||||||
|
None => errors = Some(e),
|
||||||
|
Some(existing) => existing.combine(e),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match errors {
|
||||||
|
None => Ok(results),
|
||||||
|
Some(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
26
crates/pmp-core/src/functions.rs
Normal file
26
crates/pmp-core/src/functions.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
pub 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
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn snake_to_title(s: &str) -> String
|
||||||
|
{
|
||||||
|
s.split('_')
|
||||||
|
.map(|word| {
|
||||||
|
let mut c = word.chars();
|
||||||
|
match c.next() {
|
||||||
|
None => String::new(),
|
||||||
|
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
}
|
||||||
20
crates/pmp-core/src/lib.rs
Normal file
20
crates/pmp-core/src/lib.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
//! Shared proc-macro utilities for all `pmp-*` crates.
|
||||||
|
//!
|
||||||
|
//! This crate provides building blocks that are identical across every
|
||||||
|
//! `pmp-*-core` proc-derive-macro crate. Each consumer crate still defines
|
||||||
|
//! its own darling input structs (because `forward_attrs` bakes in a per-crate
|
||||||
|
//! attribute name at compile time), but everything else should be imported from here.
|
||||||
|
//!
|
||||||
|
//! | Module | Contents |
|
||||||
|
//! |-----------|------------------------------------------------------------------|
|
||||||
|
//! | `types` | [`StrictBool`], [`StrictString`], [`StrictPath`], [`StrictExpr`] |
|
||||||
|
//! | `helpers` | [`pascal_to_title`], [`snake_to_title`] |
|
||||||
|
//! | `attr` | [`parse_pmp_attr`], [`parse_pmp_attrs`], [`HasAttrs`] |
|
||||||
|
|
||||||
|
mod attrs;
|
||||||
|
mod types;
|
||||||
|
mod functions;
|
||||||
|
|
||||||
|
pub use attrs::*;
|
||||||
|
pub use types::*;
|
||||||
|
pub use functions::*;
|
||||||
67
crates/pmp-core/src/types.rs
Normal file
67
crates/pmp-core/src/types.rs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
use syn::{ Lit, Path };
|
||||||
|
use darling::{ Error, Result, FromMeta };
|
||||||
|
|
||||||
|
/// A boolean that must be written as `= true` or `= false`
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct StrictBool(pub bool);
|
||||||
|
|
||||||
|
impl FromMeta for StrictBool
|
||||||
|
{
|
||||||
|
fn from_word() -> Result<Self>
|
||||||
|
{
|
||||||
|
Err(Error::custom(
|
||||||
|
"expected `= true/false`, provide a boolean value"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_value(value: &Lit) -> Result<Self>
|
||||||
|
{
|
||||||
|
match value {
|
||||||
|
Lit::Bool(b) => Ok(StrictBool(b.value)),
|
||||||
|
_ => Err(Error::unexpected_lit_type(value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A string that must be written as `= "..."`
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct StrictString(pub String);
|
||||||
|
|
||||||
|
impl FromMeta for StrictString
|
||||||
|
{
|
||||||
|
fn from_word() -> Result<Self>
|
||||||
|
{
|
||||||
|
Err(Error::custom(
|
||||||
|
"expected `= \"...\"`, provide a string value"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_value(value: &Lit) -> Result<Self> {
|
||||||
|
String::from_value(value).map(StrictString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A path that must be written as `= my_fn`
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct StrictPath(pub Path);
|
||||||
|
|
||||||
|
impl FromMeta for StrictPath
|
||||||
|
{
|
||||||
|
fn from_word() -> Result<Self>
|
||||||
|
{
|
||||||
|
Err(Error::custom(
|
||||||
|
"expected `= my_fn`, provide a function path"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_value(value: &Lit) -> Result<Self> {
|
||||||
|
Path::from_value(value).map(StrictPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_expr(expr: &syn::Expr) -> Result<Self> {
|
||||||
|
Path::from_expr(expr).map(StrictPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ license-file.workspace = true
|
|||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
pmp-core = { path = "../../../pmp-core" }
|
||||||
|
|
||||||
syn = { version = "^2", features = ["full"] }
|
syn = { version = "^2", features = ["full"] }
|
||||||
quote = "^1"
|
quote = "^1"
|
||||||
darling = "^0"
|
darling = "^0"
|
||||||
|
|||||||
46
crates/pmp-emitter/crates/pmp-emitter-core/src/attrs.rs
Normal file
46
crates/pmp-emitter/crates/pmp-emitter-core/src/attrs.rs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
use darling::{ ast, FromMeta, FromVariant, FromDeriveInput };
|
||||||
|
use pmp_core::{ StrictBool, StrictPath, StrictString };
|
||||||
|
use syn::{ Type, Ident };
|
||||||
|
|
||||||
|
#[derive(Debug, FromDeriveInput)]
|
||||||
|
#[darling(forward_attrs(pmp_emitter))]
|
||||||
|
pub(crate) struct EmitterInput {
|
||||||
|
pub ident: Ident,
|
||||||
|
pub data: ast::Data<VariantReceiver, ()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, FromVariant)]
|
||||||
|
#[darling(forward_attrs(pmp_emitter))]
|
||||||
|
pub(crate) struct VariantReceiver {
|
||||||
|
pub ident: Ident,
|
||||||
|
pub fields: ast::Fields<Type>,
|
||||||
|
pub attrs: Vec<syn::Attribute>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------- //
|
||||||
|
|
||||||
|
/// - `descriptor = "..."` - prefix shown before all variant messages (default: enum name)
|
||||||
|
/// - `exit = true|false` - whether to call process::exit (default: false)
|
||||||
|
/// - `colorizer = fn` - fn(String) -> String applied to all variants unless overridden
|
||||||
|
#[derive(Debug, Default, FromMeta)]
|
||||||
|
pub(crate) struct EnumAttrs {
|
||||||
|
#[darling(default)]
|
||||||
|
pub exit: Option<StrictBool>,
|
||||||
|
#[darling(default)]
|
||||||
|
pub colorizer: Option<StrictPath>,
|
||||||
|
#[darling(default)]
|
||||||
|
pub descriptor: Option<StrictString>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// - `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, Default, FromMeta)]
|
||||||
|
pub(crate) struct VariantAttrs {
|
||||||
|
#[darling(default)]
|
||||||
|
pub exit: Option<StrictBool>,
|
||||||
|
#[darling(default)]
|
||||||
|
pub message: Option<StrictString>,
|
||||||
|
#[darling(default)]
|
||||||
|
pub colorizer: Option<StrictPath>,
|
||||||
|
}
|
||||||
103
crates/pmp-emitter/crates/pmp-emitter-core/src/expand.rs
Normal file
103
crates/pmp-emitter/crates/pmp-emitter-core/src/expand.rs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
|
use quote::{ quote, quote_spanned };
|
||||||
|
use pmp_core::{ self, StrictPath };
|
||||||
|
use syn::DeriveInput;
|
||||||
|
use darling::ast;
|
||||||
|
|
||||||
|
use crate::attrs::{ EnumAttrs, EmitterInput, VariantAttrs };
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------- //
|
||||||
|
|
||||||
|
pub fn expand(shape: EmitterInput, raw_input: &DeriveInput) -> syn::Result<TokenStream2>
|
||||||
|
{
|
||||||
|
let enum_attrs: EnumAttrs = pmp_core::parse_pmp_attr(&raw_input.attrs, "pmp_emitter")?;
|
||||||
|
|
||||||
|
let enum_ident = &shape.ident;
|
||||||
|
let descriptor = enum_attrs.descriptor
|
||||||
|
.map(|s| s.0)
|
||||||
|
.unwrap_or_else(|| pmp_core::pascal_to_title(&enum_ident.to_string()));
|
||||||
|
let global_exit = enum_attrs.exit.map(|b| b.0).unwrap_or(false);
|
||||||
|
let global_colorizer = enum_attrs.colorizer;
|
||||||
|
|
||||||
|
let variants = shape.data.take_enum().ok_or_else(|| {
|
||||||
|
syn::Error::new_spanned(enum_ident, "PmpEmitter can only be derived on enums")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut errors: Option<syn::Error> = None;
|
||||||
|
let mut match_arms: Vec<TokenStream2> = Vec::with_capacity(variants.len());
|
||||||
|
|
||||||
|
for v in &variants
|
||||||
|
{
|
||||||
|
let var_ident = &v.ident;
|
||||||
|
let title = pmp_core::pascal_to_title(&var_ident.to_string());
|
||||||
|
|
||||||
|
let v_attrs: VariantAttrs = match pmp_core::parse_pmp_attr(&v.attrs, "pmp_emitter")
|
||||||
|
{
|
||||||
|
Ok(a) => a,
|
||||||
|
Err(e) =>
|
||||||
|
{
|
||||||
|
match &mut errors {
|
||||||
|
None => errors = Some(e),
|
||||||
|
Some(existing) => existing.combine(e),
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let message = v_attrs.message.map(|s| s.0).unwrap_or_else(|| title.clone());
|
||||||
|
let should_exit = v_attrs.exit.map(|b| b.0).unwrap_or(global_exit);
|
||||||
|
|
||||||
|
let header = format!("{}: {} (\"{}\")", descriptor, title, message);
|
||||||
|
|
||||||
|
let formatted_header = match v_attrs.colorizer.as_ref().or(global_colorizer.as_ref()) {
|
||||||
|
Some(StrictPath(path)) => quote! { #path(#header) },
|
||||||
|
None => quote! { #header.to_string() },
|
||||||
|
};
|
||||||
|
|
||||||
|
let exit_tokens = if should_exit { quote! { std::process::exit(1); } } else { quote! {} };
|
||||||
|
|
||||||
|
let arm = match v.fields.style
|
||||||
|
{
|
||||||
|
ast::Style::Tuple =>
|
||||||
|
{
|
||||||
|
let pattern = if v.fields.len() == 1 { quote! { (trace) } } else { quote! { (trace, ..) } };
|
||||||
|
|
||||||
|
quote_spanned! { var_ident.span() =>
|
||||||
|
#enum_ident::#var_ident #pattern =>
|
||||||
|
{
|
||||||
|
println!("{}\n\n{}", #formatted_header, trace);
|
||||||
|
#exit_tokens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => quote_spanned! { var_ident.span() =>
|
||||||
|
#enum_ident::#var_ident =>
|
||||||
|
{
|
||||||
|
println!("{}", #formatted_header);
|
||||||
|
#exit_tokens
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
match_arms.push(arm);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(e) = errors { return Err(e); }
|
||||||
|
|
||||||
|
Ok(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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,257 +1,14 @@
|
|||||||
|
use darling::FromDeriveInput;
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use quote::{ quote, quote_spanned };
|
use syn::DeriveInput;
|
||||||
use syn::{ Lit, Path, Type, Ident, DeriveInput };
|
|
||||||
use proc_macro2::{ TokenStream as TokenStream2 };
|
mod attrs;
|
||||||
use darling::{ ast, Error, Result, FromMeta, FromVariant, FromDeriveInput };
|
mod expand;
|
||||||
|
|
||||||
|
use attrs::EmitterInput;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------- //
|
// ---------------------------------------------------------------------------------------------- //
|
||||||
|
|
||||||
#[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>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------- //
|
|
||||||
|
|
||||||
/// A boolean that must be written as `= true` or `= false`
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
struct StrictBool(bool);
|
|
||||||
|
|
||||||
impl FromMeta for StrictBool
|
|
||||||
{
|
|
||||||
fn from_word() -> Result<Self> {
|
|
||||||
Err(Error::custom(
|
|
||||||
"expected `= true/false`, provide a boolean value"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_value(value: &Lit) -> Result<Self>
|
|
||||||
{
|
|
||||||
match value {
|
|
||||||
Lit::Bool(b) => Ok(StrictBool(b.value)),
|
|
||||||
_ => Err(Error::unexpected_lit_type(value)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A string that must be written as `= "..."`
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct StrictString(String);
|
|
||||||
|
|
||||||
impl FromMeta for StrictString
|
|
||||||
{
|
|
||||||
fn from_word() -> Result<Self> {
|
|
||||||
Err(Error::custom(
|
|
||||||
"expected `= \"...\"`, provide a string value"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_value(value: &Lit) -> Result<Self> {
|
|
||||||
String::from_value(value).map(StrictString)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A path that must be written as `= my_fn`
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct StrictPath(Path);
|
|
||||||
|
|
||||||
impl FromMeta for StrictPath
|
|
||||||
{
|
|
||||||
fn from_word() -> Result<Self> {
|
|
||||||
Err(Error::custom(
|
|
||||||
"expected `= my_fn`, provide a function path"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_value(value: &Lit) -> Result<Self> {
|
|
||||||
Path::from_value(value).map(StrictPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_expr(expr: &syn::Expr) -> Result<Self> {
|
|
||||||
Path::from_expr(expr).map(StrictPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------- //
|
|
||||||
|
|
||||||
/// - `descriptor = "..."` - prefix shown before all variant messages (default: enum name)
|
|
||||||
/// - `exit = true|false` - whether to call process::exit (default: false)
|
|
||||||
/// - `colorizer = fn` - fn(String) -> String applied to all variants unless overridden
|
|
||||||
#[derive(Debug, Default, FromMeta)]
|
|
||||||
struct EnumAttrs {
|
|
||||||
#[darling(default)]
|
|
||||||
exit: Option<StrictBool>,
|
|
||||||
#[darling(default)]
|
|
||||||
colorizer: Option<StrictPath>,
|
|
||||||
#[darling(default)]
|
|
||||||
descriptor: Option<StrictString>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// - `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, Default, FromMeta)]
|
|
||||||
struct VariantAttrs {
|
|
||||||
#[darling(default)]
|
|
||||||
exit: Option<StrictBool>,
|
|
||||||
#[darling(default)]
|
|
||||||
message: Option<StrictString>,
|
|
||||||
#[darling(default)]
|
|
||||||
colorizer: Option<StrictPath>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------- //
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse a `#[pmp_emitter(...)]` attribute from the given list into `T`.
|
|
||||||
///
|
|
||||||
/// Returns `Ok(T::default())` if no such attribute is present or
|
|
||||||
/// a span-accurate `Err` if the attribute is present but malformed.
|
|
||||||
fn parse_pmp_attr<T>(attrs: &[syn::Attribute]) -> syn::Result<T>
|
|
||||||
where
|
|
||||||
T: FromMeta + Default,
|
|
||||||
{
|
|
||||||
match attrs.iter().find(|a| a.path().is_ident("pmp_emitter")) {
|
|
||||||
None => Ok(T::default()),
|
|
||||||
Some(attr) => T::from_meta(&attr.meta)
|
|
||||||
.map_err(|e| syn::Error::new_spanned(attr, e.to_string())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------- //
|
|
||||||
|
|
||||||
/// All real expansion logic lives here, separate from the proc-macro entry point,
|
|
||||||
/// so it can be called and tested without the proc-macro machinery.
|
|
||||||
fn expand(shape: EmitterInput, raw_input: &DeriveInput) -> syn::Result<TokenStream2>
|
|
||||||
{
|
|
||||||
let enum_attrs: EnumAttrs = parse_pmp_attr(&raw_input.attrs)?;
|
|
||||||
|
|
||||||
let enum_ident = &shape.ident;
|
|
||||||
let descriptor = enum_attrs.descriptor
|
|
||||||
.map(|s| s.0)
|
|
||||||
.unwrap_or_else(|| pascal_to_title(&enum_ident.to_string()));
|
|
||||||
|
|
||||||
let global_exit = enum_attrs.exit.map(|b| b.0).unwrap_or(false);
|
|
||||||
let global_colorizer = enum_attrs.colorizer;
|
|
||||||
|
|
||||||
// Reject non-enums with a proper span pointing at the type name.
|
|
||||||
let variants = shape.data.take_enum().ok_or_else(|| {
|
|
||||||
syn::Error::new_spanned(enum_ident, "PmpEmitter can only be derived on enums")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Accumulate all per-variant attribute errors before failing, so the user
|
|
||||||
// sees every problem in one compilation instead of one-at-a-time.
|
|
||||||
let mut errors: Option<syn::Error> = None;
|
|
||||||
let mut match_arms: Vec<TokenStream2> = Vec::with_capacity(variants.len());
|
|
||||||
|
|
||||||
for v in &variants
|
|
||||||
{
|
|
||||||
let var_ident = &v.ident;
|
|
||||||
let title = pascal_to_title(&var_ident.to_string());
|
|
||||||
|
|
||||||
let v_attrs: VariantAttrs = match parse_pmp_attr(&v.attrs)
|
|
||||||
{
|
|
||||||
Ok(a) => a,
|
|
||||||
Err(e) =>
|
|
||||||
{
|
|
||||||
match &mut errors {
|
|
||||||
None => errors = Some(e),
|
|
||||||
Some(existing) => existing.combine(e),
|
|
||||||
}
|
|
||||||
continue; // keep parsing remaining variants
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let message = v_attrs.message.map(|s| s.0).unwrap_or_else(|| title.clone());
|
|
||||||
let should_exit = v_attrs.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 v_attrs.colorizer.as_ref().or(global_colorizer.as_ref()) {
|
|
||||||
Some(StrictPath(path)) => quote! { #path(#header) },
|
|
||||||
None => quote! { #header.to_string() },
|
|
||||||
};
|
|
||||||
|
|
||||||
let exit_tokens = if should_exit { quote! { std::process::exit(1); } } else { quote! {} };
|
|
||||||
|
|
||||||
// quote_spanned! ties any downstream type errors back to the variant's
|
|
||||||
// source location instead of pointing at generated code.
|
|
||||||
let arm = match v.fields.style
|
|
||||||
{
|
|
||||||
ast::Style::Tuple =>
|
|
||||||
{
|
|
||||||
let pattern = if v.fields.len() == 1 { quote! { (trace) } } else { quote! { (trace, ..) } };
|
|
||||||
|
|
||||||
quote_spanned! { var_ident.span() =>
|
|
||||||
#enum_ident::#var_ident #pattern =>
|
|
||||||
{
|
|
||||||
println!("{}\n\n{}", #formatted_header, trace);
|
|
||||||
#exit_tokens
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => quote_spanned! { var_ident.span() =>
|
|
||||||
#enum_ident::#var_ident =>
|
|
||||||
{
|
|
||||||
println!("{}", #formatted_header);
|
|
||||||
#exit_tokens
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
match_arms.push(arm);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Surface all accumulated variant errors at once.
|
|
||||||
if let Some(e) = errors { return Err(e); }
|
|
||||||
|
|
||||||
Ok(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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------- //
|
|
||||||
|
|
||||||
/// Thin entry point: parse -> delegate -> convert any error to `compile_error!`.
|
|
||||||
#[proc_macro_derive(PmpEmitter, attributes(pmp_emitter))]
|
#[proc_macro_derive(PmpEmitter, attributes(pmp_emitter))]
|
||||||
pub fn pmp_emitter(item: TokenStream) -> TokenStream
|
pub fn pmp_emitter(item: TokenStream) -> TokenStream
|
||||||
{
|
{
|
||||||
@@ -262,7 +19,7 @@ pub fn pmp_emitter(item: TokenStream) -> TokenStream
|
|||||||
Err(e) => return e.write_errors().into(),
|
Err(e) => return e.write_errors().into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
expand(shape, &input)
|
expand::expand(shape, &input)
|
||||||
.unwrap_or_else(syn::Error::into_compile_error)
|
.unwrap_or_else(syn::Error::into_compile_error)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user