aboutsummaryrefslogtreecommitdiff
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
parentdd28430b67dcfda6dd2e91750a4c1a62c085150a (diff)
Add `pack_err!` macro for error structs with automatic name field
-rw-r--r--CHANGELOG.md52
-rw-r--r--docs/example-pages/examples.json24
-rw-r--r--examples/example-pack-err/Cargo.lock141
-rw-r--r--examples/example-pack-err/Cargo.toml16
-rw-r--r--examples/example-pack-err/page.toml10
-rw-r--r--examples/example-pack-err/src/main.rs101
-rw-r--r--examples/test-examples.toml20
-rw-r--r--mingling/src/example_docs.rs125
-rw-r--r--mingling/src/lib.rs6
-rw-r--r--mingling_macros/src/lib.rs70
-rw-r--r--mingling_macros/src/pack_err.rs143
11 files changed, 704 insertions, 4 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 78698b3..b9ca68b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -124,6 +124,58 @@ fn render_entry_show(_args: EntryShow, res: &mut LazyRes<ResLargeData>) {
7. **\[core:comp\]** Added `Program::is_completing()` method to check whether the program is currently running in completion mode. This provides a convenient way to conditionally skip certain logic during completion generation, where those operations may be unnecessary or undesirable.
+8. **\[macros\]** Added the `pack_err!` macro for creating error structs with automatic `name` field.
+
+The `pack_err!` macro provides a concise way to define error types that implement `Groupped` and are automatically 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.
+
+Two forms are supported:
+
+```rust
+// 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);
+```
+
+For `pack_err!(ErrorNotFound)`, the generated code is:
+
+```rust
+#[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
+#[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,
+ }
+ }
+}
+```
+
+This macro is only available with the `extra_macros` feature.
+
#### **BREAKING CHANGES** (API CHANGES):
1. **\[core\]** Changed the signature of `ProgramSetup::setup` from `fn setup(&mut self, program: &mut Program<C>) -> S` to `fn setup(self, program: &mut Program<C>)`, consuming `self` instead of taking a mutable reference. Correspondingly, `Program::with_setup` now accepts `S` by value (`&mut self, setup: S`) instead of by mutable reference (`&mut self, setup: &mut S`).
diff --git a/docs/example-pages/examples.json b/docs/example-pages/examples.json
index e7f0c0c..8e8719b 100644
--- a/docs/example-pages/examples.json
+++ b/docs/example-pages/examples.json
@@ -220,6 +220,23 @@
]
},
{
+ "id": "example-pack-err",
+ "name": "pack_err!",
+ "icon": "🛑",
+ "category": "macros",
+ "desc": "Demonstrates how to use the `pack_err!` macro to define error types with automatic `name` field (snake_case at compile time) and optional `info` field. Also shows `--json` serialization when `general_renderer` is enabled.\n",
+ "tags": [
+ "pack_err!",
+ "extra_macros",
+ "general_renderer",
+ "--json"
+ ],
+ "files": [
+ "src/main.rs",
+ "Cargo.toml"
+ ]
+ },
+ {
"id": "example-panic-unwind",
"name": "Panic Unwind",
"icon": "💥",
@@ -258,9 +275,8 @@
"injection"
],
"files": [
- "Cargo.toml",
"src/main.rs",
- "src/lib.rs"
+ "Cargo.toml"
]
},
{
@@ -289,8 +305,8 @@
"extra_macros"
],
"files": [
- "Cargo.toml",
- "src/main.rs"
+ "src/main.rs",
+ "Cargo.toml"
]
},
{
diff --git a/examples/example-pack-err/Cargo.lock b/examples/example-pack-err/Cargo.lock
new file mode 100644
index 0000000..36d4b6f
--- /dev/null
+++ b/examples/example-pack-err/Cargo.lock
@@ -0,0 +1,141 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "example-pack-err"
+version = "0.1.0"
+dependencies = [
+ "mingling",
+ "serde",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "just_fmt"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+
+[[package]]
+name = "memchr"
+version = "2.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
+
+[[package]]
+name = "mingling"
+version = "0.2.0"
+dependencies = [
+ "mingling_core",
+ "mingling_macros",
+ "serde",
+]
+
+[[package]]
+name = "mingling_core"
+version = "0.2.0"
+dependencies = [
+ "just_fmt",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "mingling_macros"
+version = "0.2.0"
+dependencies = [
+ "just_fmt",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/examples/example-pack-err/Cargo.toml b/examples/example-pack-err/Cargo.toml
new file mode 100644
index 0000000..883fc89
--- /dev/null
+++ b/examples/example-pack-err/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "example-pack-err"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+serde = { version = "1.0.228", features = ["derive"] }
+
+[dependencies.mingling]
+path = "../../mingling"
+features = [
+ "general_renderer",
+ "extra_macros",
+]
+
+[workspace]
diff --git a/examples/example-pack-err/page.toml b/examples/example-pack-err/page.toml
new file mode 100644
index 0000000..5534236
--- /dev/null
+++ b/examples/example-pack-err/page.toml
@@ -0,0 +1,10 @@
+[example]
+id = "example-pack-err"
+name = "pack_err!"
+icon = "🛑"
+category = "macros"
+desc = """
+Demonstrates how to use the `pack_err!` macro to define error types with automatic `name` field (snake_case at compile time) and optional `info` field. Also shows `--json` serialization when `general_renderer` is enabled.
+"""
+tags = ["pack_err!", "extra_macros", "general_renderer", "--json"]
+files = ["src/main.rs", "Cargo.toml"]
diff --git a/examples/example-pack-err/src/main.rs b/examples/example-pack-err/src/main.rs
new file mode 100644
index 0000000..72fecd6
--- /dev/null
+++ b/examples/example-pack-err/src/main.rs
@@ -0,0 +1,101 @@
+//! Example `pack_err!`
+//!
+//! > This example demonstrates how to use the `pack_err!` macro to define error types
+//! > with automatic `name` field (set to snake_case at compile time) and optional `info` field.
+//! > Also demonstrates `--json` serialization when `general_renderer` is enabled.
+//!
+//! Run:
+//! ```bash
+//! cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find
+//! cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find --json
+//! cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find Cargo.toml
+//! cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find Cargo.toml --json
+//! cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find src
+//! cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find src --json
+//! ```
+//!
+//! Output:
+//! ```plaintext
+//! Search path not provided
+//! {"name":"error_not_found"}
+//! Not a directory: Cargo.toml
+//! {"name":"error_not_dir","info":"Cargo.toml"}
+//! Found directory: src
+//! {"inner":"src"}
+//! ```
+
+use mingling::prelude::*;
+use mingling::setup::GeneralRendererSetup;
+use std::path::PathBuf;
+
+dispatcher!("find", CMDFind => EntryFind);
+
+// --------- IMPORTANT ---------
+// `pack_err!` is a convenient macro for defining error types.
+//
+// Simple form: pack_err!(ErrorNotFound);
+// Typed form: pack_err!(ErrorNotDir = PathBuf);
+//
+// The simple form generates a struct with `name: String` and `impl Default`.
+// name = "error_not_found" (automatically snake_cased at compile time)
+//
+// The typed form additionally generates `pub fn new(info)`.
+// name = "error_not_dir"
+//
+// When `general_renderer` is enabled, the struct also gets
+// `#[derive(serde::Serialize)]` for --json / --yaml output.
+// --------- IMPORTANT ---------
+
+// Simple form — name = "error_not_found"
+pack_err!(ErrorNotFound);
+
+// Typed form — name = "error_not_dir"
+pack_err!(ErrorNotDir = PathBuf);
+
+// Success type using traditional pack!
+pack!(ResultPath = PathBuf);
+
+#[chain]
+fn handle_find(args: EntryFind) -> Next {
+ let Some(path_str) = args.inner.first().cloned() else {
+ // No path provided → use the simple error form (Default)
+ return ErrorNotFound::default().to_render();
+ };
+
+ let path = PathBuf::from(&path_str);
+ if path.is_dir() {
+ // Is a directory → success
+ ResultPath::new(path).to_render()
+ } else {
+ // Not a directory (or doesn't exist) → use the typed error form
+ ErrorNotDir::new(path).to_render()
+ }
+}
+
+/// Renders the successful result with the found directory path.
+#[renderer]
+fn render_result_path(path: ResultPath) {
+ r_println!("Found directory: {}", path.display());
+}
+
+/// Renders the error when no search path is provided.
+#[renderer]
+fn render_error_not_found(_: ErrorNotFound) {
+ r_println!("Search path not provided");
+}
+
+/// Renders the error when the given path is not a directory.
+#[renderer]
+fn render_error_not_dir(err: ErrorNotDir) {
+ r_println!("Not a directory: {}", err.info.display());
+}
+
+gen_program!();
+
+fn main() {
+ let mut program = ThisProgram::new();
+ // Add GeneralRendererSetup to support --json / --yaml flags
+ program.with_setup(GeneralRendererSetup);
+ program.with_dispatcher(CMDFind);
+ let _ = program.exec();
+}
diff --git a/examples/test-examples.toml b/examples/test-examples.toml
index 8d2cf0d..4a50ab1 100644
--- a/examples/test-examples.toml
+++ b/examples/test-examples.toml
@@ -207,3 +207,23 @@ expect.result = "Hello, Alice!"
command = "greet Alice -r 5"
expect.exit-code = 0
expect.result = "Hello, Alice, Alice, Alice, Alice, Alice!"
+
+[[test.example-pack-err]]
+command = "find"
+expect.exit-code = 0
+expect.result = "Search path not provided"
+
+[[test.example-pack-err]]
+command = "find --json"
+expect.exit-code = 0
+expect.result = '{"name":"error_not_found"}'
+
+[[test.example-pack-err]]
+command = "find Cargo.toml"
+expect.exit-code = 0
+expect.result = "Not a directory: Cargo.toml"
+
+[[test.example-pack-err]]
+command = "find Cargo.toml --json"
+expect.exit-code = 0
+expect.result = '{"name":"error_not_dir","info":"Cargo.toml"}'
diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs
index f4a27bd..b1d693b 100644
--- a/mingling/src/example_docs.rs
+++ b/mingling/src/example_docs.rs
@@ -1550,6 +1550,131 @@ pub mod example_implicit_dispatcher {}
/// gen_program!();
/// ```
pub mod example_lazy_resources {}
+/// Example `pack_err!`
+///
+/// > This example demonstrates how to use the `pack_err!` macro to define error types
+/// > with automatic `name` field (set to snake_case at compile time) and optional `info` field.
+/// > Also demonstrates `--json` serialization when `general_renderer` is enabled.
+///
+/// Run:
+/// ```bash
+/// cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find
+/// cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find --json
+/// cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find Cargo.toml
+/// cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find Cargo.toml --json
+/// cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find src
+/// cargo run --manifest-path examples/example-pack-err/Cargo.toml --quiet -- find src --json
+/// ```
+///
+/// Output:
+/// ```plaintext
+/// Search path not provided
+/// {"name":"error_not_found"}
+/// Not a directory: Cargo.toml
+/// {"name":"error_not_dir","info":"Cargo.toml"}
+/// Found directory: src
+/// {"inner":"src"}
+/// ```
+///
+/// Source code (./Cargo.toml)
+/// ```toml
+/// [package]
+/// name = "example-pack-err"
+/// version = "0.1.0"
+/// edition = "2024"
+///
+/// [dependencies]
+/// serde = { version = "1.0.228", features = ["derive"] }
+///
+/// [dependencies.mingling]
+/// path = "../../mingling"
+/// features = [
+/// "general_renderer",
+/// "extra_macros",
+/// ]
+///
+/// [workspace]
+/// ```
+///
+/// Source code (./src/main.rs)
+/// ```ignore
+/// use mingling::prelude::*;
+/// use mingling::setup::GeneralRendererSetup;
+/// use std::path::PathBuf;
+///
+/// dispatcher!("find", CMDFind => EntryFind);
+///
+/// // --------- IMPORTANT ---------
+/// // `pack_err!` is a convenient macro for defining error types.
+/// //
+/// // Simple form: pack_err!(ErrorNotFound);
+/// // Typed form: pack_err!(ErrorNotDir = PathBuf);
+/// //
+/// // The simple form generates a struct with `name: String` and `impl Default`.
+/// // name = "error_not_found" (automatically snake_cased at compile time)
+/// //
+/// // The typed form additionally generates `pub fn new(info)`.
+/// // name = "error_not_dir"
+/// //
+/// // When `general_renderer` is enabled, the struct also gets
+/// // `#[derive(serde::Serialize)]` for --json / --yaml output.
+/// // --------- IMPORTANT ---------
+///
+/// // Simple form — name = "error_not_found"
+/// pack_err!(ErrorNotFound);
+///
+/// // Typed form — name = "error_not_dir"
+/// pack_err!(ErrorNotDir = PathBuf);
+///
+/// // Success type using traditional pack!
+/// pack!(ResultPath = PathBuf);
+///
+/// #[chain]
+/// fn handle_find(args: EntryFind) -> Next {
+/// let Some(path_str) = args.inner.first().cloned() else {
+/// // No path provided → use the simple error form (Default)
+/// return ErrorNotFound::default().to_render();
+/// };
+///
+/// let path = PathBuf::from(&path_str);
+/// if path.is_dir() {
+/// // Is a directory → success
+/// ResultPath::new(path).to_render()
+/// } else {
+/// // Not a directory (or doesn't exist) → use the typed error form
+/// ErrorNotDir::new(path).to_render()
+/// }
+/// }
+///
+/// /// Renders the successful result with the found directory path.
+/// #[renderer]
+/// fn render_result_path(path: ResultPath) {
+/// r_println!("Found directory: {}", path.display());
+/// }
+///
+/// /// Renders the error when no search path is provided.
+/// #[renderer]
+/// fn render_error_not_found(_: ErrorNotFound) {
+/// r_println!("Search path not provided");
+/// }
+///
+/// /// Renders the error when the given path is not a directory.
+/// #[renderer]
+/// fn render_error_not_dir(err: ErrorNotDir) {
+/// r_println!("Not a directory: {}", err.info.display());
+/// }
+///
+/// gen_program!();
+///
+/// fn main() {
+/// let mut program = ThisProgram::new();
+/// // Add GeneralRendererSetup to support --json / --yaml flags
+/// program.with_setup(GeneralRendererSetup);
+/// program.with_dispatcher(CMDFind);
+/// let _ = program.exec();
+/// }
+/// ```
+pub mod example_pack_err {}
/// Example Panic Unwind
///
/// > This example introduces how to catch Panic in the Mingling program loop
diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs
index 7b96d34..f1232d3 100644
--- a/mingling/src/lib.rs
+++ b/mingling/src/lib.rs
@@ -99,6 +99,9 @@ pub mod macros {
pub use mingling_macros::node;
/// Used to create a wrapper type for use with `Chain` and `Renderer`
pub use mingling_macros::pack;
+ /// Used to create an error struct with automatic `name` field
+ #[cfg(feature = "extra_macros")]
+ pub use mingling_macros::pack_err;
#[cfg(feature = "comp")]
/// Internal macro for '`gen_program`' used to finally generate the completion structure
pub use mingling_macros::program_comp_gen;
@@ -192,6 +195,9 @@ pub mod prelude {
pub use crate::macros::gen_program;
/// Re-export of the `pack` macro for creating wrapper types.
pub use crate::macros::pack;
+ /// Re-export of the `pack_err` macro for creating error types.
+ #[cfg(feature = "extra_macros")]
+ pub use crate::macros::pack_err;
/// Re-export of the `r_print` macro for printing within a renderer context.
pub use crate::macros::r_print;
/// Re-export of the `r_println` macro for printing with a newline within a renderer
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()
+ }
+ }
+}