aboutsummaryrefslogtreecommitdiff
path: root/mingling_macros/src
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-06-18 22:48:16 +0800
committer魏曹先生 <1992414357@qq.com>2026-06-18 22:48:16 +0800
commit0f7b2a50b05f38d886234ff6b031766c7af1dabb (patch)
tree9c9b5d4aa11c91c117b08e829ec33361c4aa6275 /mingling_macros/src
parentdd28430b67dcfda6dd2e91750a4c1a62c085150a (diff)
Add `pack_err!` macro for error structs with automatic name field
Diffstat (limited to 'mingling_macros/src')
-rw-r--r--mingling_macros/src/lib.rs70
-rw-r--r--mingling_macros/src/pack_err.rs143
2 files changed, 213 insertions, 0 deletions
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<Type>,
+ },
+}
+
+impl syn::parse::Parse for PackErrInput {
+ fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+ let type_name: Ident = input.parse()?;
+
+ if input.peek(Token![=]) {
+ input.parse::<Token![=]>()?;
+ 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()
+ }
+ }
+}