Refactor attribute parsing in pmp-emitter-core: enforce strict types, add StrictBool, StrictString, and StrictPath wrappers, and improve error reporting

This commit is contained in:
2026-03-04 13:25:12 +01:00
parent 134e35cc9e
commit 6863d7377b

View File

@@ -29,6 +29,12 @@ 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 {
@@ -36,22 +42,59 @@ impl FromMeta for StrictBool
_ => Err(Error::unexpected_lit_type(value)),
}
}
}
// intentionally NOT implementing from_word() - bare `exit` is rejected
/// 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<Path>,
colorizer: Option<StrictPath>,
#[darling(default)]
descriptor: Option<String>,
descriptor: Option<StrictString>,
}
/// - `message = "..."` - error message (default: variant name as title case)
@@ -62,9 +105,9 @@ struct VariantAttrs {
#[darling(default)]
exit: Option<StrictBool>,
#[darling(default)]
message: Option<String>,
message: Option<StrictString>,
#[darling(default)]
colorizer: Option<Path>,
colorizer: Option<StrictPath>,
}
// ---------------------------------------------------------------------------------------------- //
@@ -106,7 +149,9 @@ fn expand(shape: EmitterInput, raw_input: &DeriveInput) -> syn::Result<TokenStre
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;
@@ -128,7 +173,8 @@ fn expand(shape: EmitterInput, raw_input: &DeriveInput) -> syn::Result<TokenStre
let v_attrs: VariantAttrs = match parse_pmp_attr(&v.attrs)
{
Ok(a) => a,
Err(e) => {
Err(e) =>
{
match &mut errors {
None => errors = Some(e),
Some(existing) => existing.combine(e),
@@ -137,7 +183,7 @@ fn expand(shape: EmitterInput, raw_input: &DeriveInput) -> syn::Result<TokenStre
}
};
let message = v_attrs.message.unwrap_or_else(|| title.clone());
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 ───────────────────────────────┐
@@ -148,7 +194,7 @@ fn expand(shape: EmitterInput, raw_input: &DeriveInput) -> syn::Result<TokenStre
let header = format!("{}: {} (\"{}\")", descriptor, title, message);
let formatted_header = match v_attrs.colorizer.as_ref().or(global_colorizer.as_ref()) {
Some(path) => quote! { #path(#header) },
Some(StrictPath(path)) => quote! { #path(#header) },
None => quote! { #header.to_string() },
};
@@ -158,14 +204,21 @@ fn expand(shape: EmitterInput, raw_input: &DeriveInput) -> syn::Result<TokenStre
// source location instead of pointing at generated code.
let arm = match v.fields.style
{
ast::Style::Tuple => quote_spanned! { var_ident.span() =>
#enum_ident::#var_ident(trace) => {
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 => {
#enum_ident::#var_ident =>
{
println!("{}", #formatted_header);
#exit_tokens
}