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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user