From 0f7b2a50b05f38d886234ff6b031766c7af1dabb Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Thu, 18 Jun 2026 22:48:16 +0800 Subject: Add `pack_err!` macro for error structs with automatic name field --- mingling_macros/src/lib.rs | 70 ++++++++++++++++++++ mingling_macros/src/pack_err.rs | 143 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 mingling_macros/src/pack_err.rs (limited to 'mingling_macros/src') diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index 97dd824..408450a 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -158,6 +158,8 @@ mod help; mod node; mod pack; #[cfg(feature = "extra_macros")] +mod pack_err; +#[cfg(feature = "extra_macros")] mod program_setup; mod render; mod renderer; @@ -319,6 +321,74 @@ pub fn pack(input: TokenStream) -> TokenStream { pack::pack(input) } +/// Creates an error struct with a `name: String` field and optional `info: Type` field. +/// +/// This macro provides a concise way to define error types that implement `Groupped` +/// and are registered for inclusion in the program enum. +/// +/// The `name` field is automatically set to the snake_case version of the struct name +/// at compile time. +/// +/// # Syntax +/// +/// Two forms are supported: +/// +/// ```rust,ignore +/// // Simple form — generates a struct with only `name: String` and a `Default` impl: +/// pack_err!(ErrorNotFound); +/// +/// // Typed form — generates a struct with `name: String` + `info: Type` and a `new(info)` constructor: +/// pack_err!(ErrorNotDir = PathBuf); +/// ``` +/// +/// # Generated code +/// +/// For `pack_err!(ErrorNotFound)`: +/// +/// ```rust,ignore +/// #[derive(::mingling::Groupped)] +/// pub struct ErrorNotFound { +/// name: String, +/// } +/// +/// impl Default for ErrorNotFound { +/// fn default() -> Self { +/// Self { +/// name: "error_not_found".into(), +/// } +/// } +/// } +/// ``` +/// +/// For `pack_err!(ErrorNotDir = PathBuf)`: +/// +/// ```rust,ignore +/// #[derive(::mingling::Groupped)] +/// pub struct ErrorNotDir { +/// name: String, +/// info: PathBuf, +/// } +/// +/// impl ErrorNotDir { +/// pub fn new(info: PathBuf) -> Self { +/// Self { +/// name: "error_not_dir".into(), +/// info, +/// } +/// } +/// } +/// ``` +/// +/// When the `general_renderer` feature is enabled, the struct also gets +/// `#[derive(serde::Serialize)]`. +/// +/// This macro is only available with the `extra_macros` feature. +#[cfg(feature = "extra_macros")] +#[proc_macro] +pub fn pack_err(input: TokenStream) -> TokenStream { + pack_err::pack_err(input) +} + /// Early-returns an error from a `Result`, converting the `Ok` branch to a /// `ChainProcess`. /// diff --git a/mingling_macros/src/pack_err.rs b/mingling_macros/src/pack_err.rs new file mode 100644 index 0000000..dd7b083 --- /dev/null +++ b/mingling_macros/src/pack_err.rs @@ -0,0 +1,143 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{Ident, Token, Type, parse_macro_input}; + +/// Converts a PascalCase/UpperCamelCase identifier string to snake_case. +/// +/// Examples: +/// - `ErrorNotFound` → `"error_not_found"` +/// - `ErrorNotDir` → `"error_not_dir"` +/// - `FileIO` → `"file_io"` +/// - `XMLParser` → `"xml_parser"` +fn to_snake_case(ident: &str) -> String { + let mut result = String::new(); + let mut prev_is_upper = false; + + for (i, c) in ident.chars().enumerate() { + if c.is_uppercase() { + if i > 0 && !prev_is_upper { + result.push('_'); + } + for lower_c in c.to_lowercase() { + result.push(lower_c); + } + prev_is_upper = true; + } else { + result.push(c); + prev_is_upper = false; + } + } + + result +} + +enum PackErrInput { + /// pack_err!(ErrorNotFound) + Simple { type_name: Ident }, + /// pack_err!(ErrorNotDir = PathBuf) + Typed { + type_name: Ident, + inner_type: Box, + }, +} + +impl syn::parse::Parse for PackErrInput { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let type_name: Ident = input.parse()?; + + if input.peek(Token![=]) { + input.parse::()?; + let inner_type: Type = input.parse()?; + Ok(PackErrInput::Typed { + type_name, + inner_type: Box::new(inner_type), + }) + } else { + Ok(PackErrInput::Simple { type_name }) + } + } +} + +#[allow(clippy::too_many_lines)] +pub fn pack_err(input: TokenStream) -> TokenStream { + let parsed = parse_macro_input!(input as PackErrInput); + + match parsed { + PackErrInput::Simple { type_name } => { + let name_str = type_name.to_string(); + let snake_name = to_snake_case(&name_str); + + #[cfg(not(feature = "general_renderer"))] + let derive = quote! { + #[derive(::mingling::Groupped)] + }; + + #[cfg(feature = "general_renderer")] + let derive = quote! { + #[derive(::mingling::Groupped, ::serde::Serialize)] + }; + + let expanded = quote! { + #derive + pub struct #type_name { + /// The snake_case name of this error, automatically set at compile time. + name: String, + } + + impl ::std::default::Default for #type_name { + fn default() -> Self { + Self { + name: #snake_name.into(), + } + } + } + + ::mingling::macros::register_type!(#type_name); + }; + + expanded.into() + } + PackErrInput::Typed { + type_name, + inner_type, + } => { + let name_str = type_name.to_string(); + let snake_name = to_snake_case(&name_str); + + #[cfg(not(feature = "general_renderer"))] + let derive = quote! { + #[derive(::mingling::Groupped)] + }; + + #[cfg(feature = "general_renderer")] + let derive = quote! { + #[derive(::mingling::Groupped, ::serde::Serialize)] + }; + + let expanded = quote! { + #derive + pub struct #type_name { + /// The snake_case name of this error, automatically set at compile time. + name: String, + /// Additional context info for this error. + info: #inner_type, + } + + impl #type_name { + /// Creates a new error with the given info. + /// The `name` field is automatically set to the snake_case of the struct name. + pub fn new(info: #inner_type) -> Self { + Self { + name: #snake_name.into(), + info, + } + } + } + + ::mingling::macros::register_type!(#type_name); + }; + + expanded.into() + } + } +} -- cgit