aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md18
-rw-r--r--Cargo.toml1
-rw-r--r--examples/example-dispatch-tree/src/main.rs7
-rw-r--r--examples/example-exit-code/src/main.rs6
-rw-r--r--examples/example-repl/Cargo.lock92
-rw-r--r--examples/example-repl/Cargo.toml8
-rw-r--r--examples/example-repl/src/main.rs142
-rw-r--r--mingling/src/example_docs.rs173
-rw-r--r--mingling/src/lib.rs4
-rw-r--r--mingling_core/src/program.rs2
-rw-r--r--mingling_core/src/program/repl_exec.rs24
-rw-r--r--mingling_core/src/program/repl_exec/res.rs6
-rw-r--r--mingling_macros/src/lib.rs47
13 files changed, 510 insertions, 20 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a9738ff..e10c307 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,8 +8,22 @@ None
### Optimizings:
None
-#### Features:
-None
+1. **\[macros\]** Added the `empty_result!()` macro for early return from a chain function. This macro is a shorthand for constructing an `EmptyResult` and converting it into a `ChainProcess`, signaling to the pipeline that there is no meaningful output to continue processing.
+
+```rust
+use mingling::macros::empty_result;
+
+#[chain]
+fn maybe_skip(prev: SomeEntry) -> Next {
+ if should_skip() {
+ return empty_result!();
+ }
+ // ... continue processing
+ NextEntry::new(result)
+}
+```
+
+Expands to: `crate::EmptyResult::new(()).to_chain()`
#### **BREAKING CHANGES**:
1. **\[core\]** Panic Unwind will not be supported when the `async` feature is enabled
diff --git a/Cargo.toml b/Cargo.toml
index df49606..4777818 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,6 +9,7 @@ exclude = [
"examples/example-exit-code",
"examples/example-general-renderer",
"examples/example-picker",
+ "examples/example-repl",
"examples/example-resources",
"dev_tools",
]
diff --git a/examples/example-dispatch-tree/src/main.rs b/examples/example-dispatch-tree/src/main.rs
index 90879e5..d8be32a 100644
--- a/examples/example-dispatch-tree/src/main.rs
+++ b/examples/example-dispatch-tree/src/main.rs
@@ -13,7 +13,7 @@
//!
//! ```bash
//! cargo expand --manifest-path examples/example-dispatch-tree/Cargo.toml > expanded.rs
-//! cat expanded.rs | grep dispatch_args_trie -A 264
+//! cat expanded.rs
//! ```
#![allow(unused_mut)]
@@ -23,11 +23,10 @@ use mingling::prelude::*;
fn main() {
let mut program = ThisProgram::new();
- // After enabling `dispatch_tree`, this method will no longer exist
+ // // After enabling `dispatch_tree`, this method will no longer exist
// program.with_dispatcher(CommandGreet);
//
- // The `CompletionDispatcher` automatically generated by `comp` will also be imported
- // automatically
+ // // The `CompletionDispatcher` automatically generated by `comp` will also be imported automatically
// program.with_dispatcher(CompletionDispatcher);
program.exec();
diff --git a/examples/example-exit-code/src/main.rs b/examples/example-exit-code/src/main.rs
index 028692e..39428e9 100644
--- a/examples/example-exit-code/src/main.rs
+++ b/examples/example-exit-code/src/main.rs
@@ -13,7 +13,7 @@
use mingling::prelude::*;
use mingling::{
- res::{exit_code, update_exit_code},
+ res::{ExitCode, exit_code},
setup::ExitCodeSetup,
};
@@ -28,8 +28,8 @@ dispatcher!("error", ErrorCommand => ErrorEntry);
pack!(ResultError = ());
#[chain]
-fn handle_error_entry(_prev: ErrorEntry) -> Next {
- update_exit_code::<ThisProgram>(1);
+fn handle_error_entry(_prev: ErrorEntry, ec: &mut ExitCode) -> Next {
+ ec.exit_code = 1;
return ResultError::default();
}
diff --git a/examples/example-repl/Cargo.lock b/examples/example-repl/Cargo.lock
new file mode 100644
index 0000000..cabbe98
--- /dev/null
+++ b/examples/example-repl/Cargo.lock
@@ -0,0 +1,92 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "example-repl"
+version = "0.0.1"
+dependencies = [
+ "just_fmt",
+ "mingling",
+]
+
+[[package]]
+name = "just_fmt"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+
+[[package]]
+name = "mingling"
+version = "0.1.9"
+dependencies = [
+ "mingling_core",
+ "mingling_macros",
+ "size",
+]
+
+[[package]]
+name = "mingling_core"
+version = "0.1.9"
+dependencies = [
+ "just_fmt",
+ "once_cell",
+]
+
+[[package]]
+name = "mingling_macros"
+version = "0.1.9"
+dependencies = [
+ "just_fmt",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[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 = "size"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b6709c7b6754dca1311b3c73e79fcce40dd414c782c66d88e8823030093b02b"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+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"
diff --git a/examples/example-repl/Cargo.toml b/examples/example-repl/Cargo.toml
new file mode 100644
index 0000000..34b85e3
--- /dev/null
+++ b/examples/example-repl/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "example-repl"
+version = "0.0.1"
+edition = "2024"
+
+[dependencies]
+mingling = { path = "../../mingling", features = ["repl", "parser"] }
+just_fmt = "0.1.2"
diff --git a/examples/example-repl/src/main.rs b/examples/example-repl/src/main.rs
new file mode 100644
index 0000000..2eb92e1
--- /dev/null
+++ b/examples/example-repl/src/main.rs
@@ -0,0 +1,142 @@
+use mingling::{REPL, hook::ProgramHook, prelude::*, this};
+use std::{env::current_dir, path::PathBuf};
+
+// Resource to store the current directory
+#[derive(Clone)]
+struct CurrentDir {
+ dir: PathBuf,
+}
+
+impl Default for CurrentDir {
+ fn default() -> Self {
+ Self {
+ dir: current_dir().unwrap(),
+ }
+ }
+}
+
+fn main() {
+ let mut program = ThisProgram::new();
+
+ // Add resource
+ program.with_resource(CurrentDir::default());
+
+ // Add dispatchers
+ program.with_dispatcher(ChangeDirectoryCommand);
+ program.with_dispatcher(ListCommand);
+ program.with_dispatcher(ExitCommand);
+
+ // Add hooks to handle REPL-related events
+ program.with_hook(
+ ProgramHook::empty()
+ .on_repl_begin(|| {
+ // Print welcome message
+ println!("Welcome!")
+ })
+ .on_repl_pre_readline(|| {
+ // Print prompt
+ let res = this::<ThisProgram>().res::<CurrentDir>().unwrap();
+ let dir_str: String = res.dir.to_string_lossy().into();
+ let prompt = format!(
+ "{}> ",
+ dir_str
+ .replace(&['/', '\\'][..], ">")
+ .trim_start_matches('>')
+ .trim_end_matches('>')
+ );
+ print!("{}", prompt)
+ })
+ .on_repl_receive_result(|r| {
+ // Print output
+ if !r.is_empty() {
+ println!("{}", r.trim())
+ }
+ }),
+ );
+
+ // Start the REPL loop
+ program.exec_repl();
+}
+
+// Create error route
+pack!(ErrorDirectoryNotExist = PathBuf);
+
+// Create commands: cd ls exit
+dispatcher!("cd", ChangeDirectoryCommand => ChangeDirectoryEntry);
+dispatcher!("ls", ListCommand => ListEntry);
+dispatcher!("exit", ExitCommand => ExitEntry);
+
+// Define data needed for the cd command's execution phase
+pack!(StateChangeDirectory = String);
+
+// Define data needed for the ls command's rendering phase
+pack!(ResultList = Vec<String>);
+
+// Parse cd command arguments
+#[chain]
+fn parse_cd_args(prev: ChangeDirectoryEntry) -> Next {
+ let join = prev.pick(()).unpack();
+ StateChangeDirectory::new(join)
+}
+
+// Execute directory change
+#[chain]
+fn handle_cd(prev: StateChangeDirectory, current_dir: &mut CurrentDir) -> Next {
+ let join = prev.inner;
+ let new_dir = just_fmt::fmt_path::fmt_path(current_dir.dir.join(join)).unwrap_or_default();
+
+ // If the path is not found, route to error handling
+ if !new_dir.exists() {
+ return ErrorDirectoryNotExist::new(new_dir).to_render();
+ }
+
+ current_dir.dir = new_dir;
+ empty_result!()
+}
+
+// Get directory contents via the CurrentDir resource
+#[chain]
+fn handle_ls(_prev: ListEntry, current_dir: &CurrentDir) -> Next {
+ let dir = &current_dir.dir;
+ let entries: Vec<String> = std::fs::read_dir(dir)
+ .into_iter()
+ .flat_map(|rd| rd.filter_map(|e| e.ok()))
+ .map(|e| {
+ let name = e.file_name().to_string_lossy().to_string();
+ if e.file_type().map(|t| t.is_dir()).unwrap_or(false) {
+ format!("{}/", name)
+ } else {
+ name
+ }
+ })
+ .collect();
+
+ // Render ResultList
+ ResultList::new(entries).to_render()
+}
+
+// Render ResultList data
+#[renderer]
+fn render_list(list: ResultList) {
+ for item in list.inner {
+ r_println!("{}", item)
+ }
+}
+
+// Handle exit command event
+#[chain]
+fn handle_exit(
+ _prev: ExitEntry,
+ repl: &mut REPL, // Import REPL resource, registered in `exec_repl`, usable directly
+) {
+ // Set the REPL exit flag; REPL will exit after this loop iteration
+ repl.exit = true;
+}
+
+// Handle path not found event
+#[renderer]
+fn render_error_directory_not_exist(err: ErrorDirectoryNotExist) {
+ r_println!("Directory not found: {}", err.inner.display())
+}
+
+gen_program!();
diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs
index 806553f..ab725de 100644
--- a/mingling/src/example_docs.rs
+++ b/mingling/src/example_docs.rs
@@ -294,7 +294,7 @@ pub mod example_completion {}
///
/// ```bash
/// cargo expand --manifest-path examples/example-dispatch-tree/Cargo.toml > expanded.rs
-/// cat expanded.rs | grep dispatch_args_trie -A 264
+/// cat expanded.rs
/// ```
///
/// Cargo.toml
@@ -317,11 +317,10 @@ pub mod example_completion {}
/// fn main() {
/// let mut program = ThisProgram::new();
///
-/// // After enabling `dispatch_tree`, this method will no longer exist
+/// // // After enabling `dispatch_tree`, this method will no longer exist
/// // program.with_dispatcher(CommandGreet);
/// //
-/// // The `CompletionDispatcher` automatically generated by `comp` will also be imported
-/// // automatically
+/// // // The `CompletionDispatcher` automatically generated by `comp` will also be imported automatically
/// // program.with_dispatcher(CompletionDispatcher);
///
/// program.exec();
@@ -370,7 +369,7 @@ pub mod example_dispatch_tree {}
/// ```ignore
/// use mingling::prelude::*;
/// use mingling::{
-/// res::{exit_code, update_exit_code},
+/// res::{ExitCode, exit_code},
/// setup::ExitCodeSetup,
/// };
///
@@ -385,8 +384,8 @@ pub mod example_dispatch_tree {}
/// pack!(ResultError = ());
///
/// #[chain]
-/// fn handle_error_entry(_prev: ErrorEntry) -> Next {
-/// update_exit_code::<ThisProgram>(1);
+/// fn handle_error_entry(_prev: ErrorEntry, ec: &mut ExitCode) -> Next {
+/// ec.exit_code = 1;
/// return ResultError::default();
/// }
///
@@ -569,6 +568,166 @@ pub mod example_general_renderer {}
/// gen_program!();
/// ```
pub mod example_picker {}
+
+///
+/// Cargo.toml
+/// ```ignore
+/// [package]
+/// name = "example-repl"
+/// version = "0.0.1"
+/// edition = "2024"
+///
+/// [dependencies]
+/// mingling = { path = "../../mingling", features = ["repl", "parser"] }
+/// just_fmt = "0.1.2"
+/// ```
+///
+/// main.rs
+/// ```ignore
+/// use mingling::{REPL, hook::ProgramHook, prelude::*, this};
+/// use std::{env::current_dir, path::PathBuf};
+///
+/// // Resource to store the current directory
+/// #[derive(Clone)]
+/// struct CurrentDir {
+/// dir: PathBuf,
+/// }
+///
+/// impl Default for CurrentDir {
+/// fn default() -> Self {
+/// Self {
+/// dir: current_dir().unwrap(),
+/// }
+/// }
+/// }
+///
+/// fn main() {
+/// let mut program = ThisProgram::new();
+///
+/// // Add resource
+/// program.with_resource(CurrentDir::default());
+///
+/// // Add dispatchers
+/// program.with_dispatcher(ChangeDirectoryCommand);
+/// program.with_dispatcher(ListCommand);
+/// program.with_dispatcher(ExitCommand);
+///
+/// // Add hooks to handle REPL-related events
+/// program.with_hook(
+/// ProgramHook::empty()
+/// .on_repl_begin(|| {
+/// // Print welcome message
+/// println!("Welcome!")
+/// })
+/// .on_repl_pre_readline(|| {
+/// // Print prompt
+/// let res = this::<ThisProgram>().res::<CurrentDir>().unwrap();
+/// let dir_str: String = res.dir.to_string_lossy().into();
+/// let prompt = format!(
+/// "{}> ",
+/// dir_str
+/// .replace(&['/', '\\'][..], ">")
+/// .trim_start_matches('>')
+/// .trim_end_matches('>')
+/// );
+/// print!("{}", prompt)
+/// })
+/// .on_repl_receive_result(|r| {
+/// // Print output
+/// if !r.is_empty() {
+/// println!("{}", r.trim())
+/// }
+/// }),
+/// );
+///
+/// // Start the REPL loop
+/// program.exec_repl();
+/// }
+///
+/// // Create error route
+/// pack!(ErrorDirectoryNotExist = PathBuf);
+///
+/// // Create commands: cd ls exit
+/// dispatcher!("cd", ChangeDirectoryCommand => ChangeDirectoryEntry);
+/// dispatcher!("ls", ListCommand => ListEntry);
+/// dispatcher!("exit", ExitCommand => ExitEntry);
+///
+/// // Define data needed for the cd command's execution phase
+/// pack!(StateChangeDirectory = String);
+///
+/// // Define data needed for the ls command's rendering phase
+/// pack!(ResultList = Vec<String>);
+///
+/// // Parse cd command arguments
+/// #[chain]
+/// fn parse_cd_args(prev: ChangeDirectoryEntry) -> Next {
+/// let join = prev.pick(()).unpack();
+/// StateChangeDirectory::new(join)
+/// }
+///
+/// // Execute directory change
+/// #[chain]
+/// fn handle_cd(prev: StateChangeDirectory, current_dir: &mut CurrentDir) -> Next {
+/// let join = prev.inner;
+/// let new_dir = just_fmt::fmt_path::fmt_path(current_dir.dir.join(join)).unwrap_or_default();
+///
+/// // If the path is not found, route to error handling
+/// if !new_dir.exists() {
+/// return ErrorDirectoryNotExist::new(new_dir).to_render();
+/// }
+///
+/// current_dir.dir = new_dir;
+/// empty_result!()
+/// }
+///
+/// // Get directory contents via the CurrentDir resource
+/// #[chain]
+/// fn handle_ls(_prev: ListEntry, current_dir: &CurrentDir) -> Next {
+/// let dir = &current_dir.dir;
+/// let entries: Vec<String> = std::fs::read_dir(dir)
+/// .into_iter()
+/// .flat_map(|rd| rd.filter_map(|e| e.ok()))
+/// .map(|e| {
+/// let name = e.file_name().to_string_lossy().to_string();
+/// if e.file_type().map(|t| t.is_dir()).unwrap_or(false) {
+/// format!("{}/", name)
+/// } else {
+/// name
+/// }
+/// })
+/// .collect();
+///
+/// // Render ResultList
+/// ResultList::new(entries).to_render()
+/// }
+///
+/// // Render ResultList data
+/// #[renderer]
+/// fn render_list(list: ResultList) {
+/// for item in list.inner {
+/// r_println!("{}", item)
+/// }
+/// }
+///
+/// // Handle exit command event
+/// #[chain]
+/// fn handle_exit(
+/// _prev: ExitEntry,
+/// repl: &mut REPL, // Import REPL resource, registered in `exec_repl`, usable directly
+/// ) {
+/// // Set the REPL exit flag; REPL will exit after this loop iteration
+/// repl.exit = true;
+/// }
+///
+/// // Handle path not found event
+/// #[renderer]
+/// fn render_error_directory_not_exist(err: ErrorDirectoryNotExist) {
+/// r_println!("Directory not found: {}", err.inner.display())
+/// }
+///
+/// gen_program!();
+/// ```
+pub mod example_repl {}
/// `Mingling` Example - Global Resource Injection
///
/// This example demonstrates how to use global resource injection in `#[chain]` functions.
diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs
index 4803c23..a8579d4 100644
--- a/mingling/src/lib.rs
+++ b/mingling/src/lib.rs
@@ -83,6 +83,8 @@ pub mod macros {
/// Used to create a dispatcher with clap argument parsing
#[cfg(feature = "clap")]
pub use mingling_macros::dispatcher_clap;
+ /// Used to create an empty result value for early return from a chain function
+ pub use mingling_macros::empty_result;
/// Used to collect data and create a command-line context
pub use mingling_macros::gen_program;
/// Used to generate a struct implementing the `HelpRequest` trait via a method
@@ -211,6 +213,8 @@ pub mod prelude {
pub use crate::macros::chain;
/// Re-export of the `dispatcher` macro for routing commands.
pub use crate::macros::dispatcher;
+ /// Re-export of the `empty_result` macro for creating an empty result value for early return.
+ pub use crate::macros::empty_result;
/// Re-export of the `gen_program` macro for generating the program entry point.
pub use crate::macros::gen_program;
/// Re-export of the `pack` macro for creating wrapper types.
diff --git a/mingling_core/src/program.rs b/mingling_core/src/program.rs
index c50e072..96b2b1a 100644
--- a/mingling_core/src/program.rs
+++ b/mingling_core/src/program.rs
@@ -26,6 +26,8 @@ mod once_exec;
#[cfg(feature = "repl")]
mod repl_exec;
+#[cfg(feature = "repl")]
+pub use repl_exec::res::REPL;
mod single_instance;
pub use single_instance::*;
diff --git a/mingling_core/src/program/repl_exec.rs b/mingling_core/src/program/repl_exec.rs
index 5246ece..5417252 100644
--- a/mingling_core/src/program/repl_exec.rs
+++ b/mingling_core/src/program/repl_exec.rs
@@ -3,11 +3,15 @@
use std::io::Write;
+#[doc(hidden)]
+pub mod res;
+
mod splitter;
use crate::error::{ProgramInternalExecuteError, ProgramPanic};
use crate::program::repl_exec::splitter::split_input_string;
use crate::{Program, ProgramCollect, RenderResult};
+use crate::{program::repl_exec::res::REPL, this};
#[cfg(not(feature = "async"))]
impl<C> Program<C>
@@ -18,10 +22,13 @@ where
///
/// This method starts an infinite loop that continuously reads user input, parses commands, executes them,
/// and displays the execution result or error message. It is suitable for scenarios requiring command-line interaction with the user.
- pub fn exec_repl(self) {
+ pub fn exec_repl(mut self) {
+ // Inject default REPL resource
+ self.with_resource(REPL::default());
+
self.run_hook_repl_on_begin();
- self.exec_wrapper(|p| -> ! {
+ self.exec_wrapper(|p| -> () {
loop {
p.run_hook_repl_pre_readline();
let readline = readline_or_empty();
@@ -38,6 +45,10 @@ where
}
_ => {}
}
+
+ if this::<C>().res::<REPL>().unwrap().exit {
+ break;
+ }
}
});
}
@@ -56,9 +67,12 @@ where
/// **Note:** When the `async` feature is enabled, panic unwinding is not supported.
/// Any panics during command execution will result in an abort rather than being caught and handled gracefully.
pub async fn exec_repl(self) {
+ // Inject default REPL resource
+ self.with_resource(REPL::default());
+
self.run_hook_repl_on_begin();
- self.exec_wrapper(async |p| -> ! {
+ self.exec_wrapper(async |p| -> () {
loop {
p.run_hook_repl_pre_readline();
let readline = readline_or_empty();
@@ -72,6 +86,10 @@ where
}
_ => {}
}
+
+ if this::<C>().res::<REPL>().unwrap().exit {
+ break;
+ }
}
})
.await;
diff --git a/mingling_core/src/program/repl_exec/res.rs b/mingling_core/src/program/repl_exec/res.rs
new file mode 100644
index 0000000..1295652
--- /dev/null
+++ b/mingling_core/src/program/repl_exec/res.rs
@@ -0,0 +1,6 @@
+/// Internal resource for the REPL runtime, used to control the REPL's state during execution
+#[derive(Default, Clone)]
+pub struct REPL {
+ /// Marks whether the REPL should exit after the current loop ends
+ pub exit: bool,
+}
diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs
index 760a6ef..badb117 100644
--- a/mingling_macros/src/lib.rs
+++ b/mingling_macros/src/lib.rs
@@ -8,7 +8,7 @@
//! The Mingling macros crate provides the following categories of macros:
//!
//! - **Command definition**: `dispatcher!`, `dispatcher_clap!`, `node!`, `pack!`
-//! - **Chain processing**: `#[chain]`, `gen_program!`, `route!`
+//! - **Chain processing**: `#[chain]`, `gen_program!`, `route!`, `empty_result!`
//! - **Rendering**: `#[renderer]`, `r_print!`, `r_println!`
//! - **Help system**: `#[help]`, `register_help!`
//! - **Derive macros**: `#[derive(Groupped)]`, `#[derive(EnumTag)]`, `#[derive(GrouppedSerialize)]`
@@ -237,6 +237,51 @@ pub fn route(input: TokenStream) -> TokenStream {
TokenStream::from(expanded)
}
+/// Creates an empty result value wrapped in `ChainProcess` for early return
+/// from a chain function.
+///
+/// This macro is a shorthand for constructing an `EmptyResult` and converting
+/// it into a `ChainProcess`, which signals to the pipeline that there is
+/// no meaningful output to continue processing.
+///
+/// # Syntax
+///
+/// ```rust,ignore
+/// empty_result!()
+/// ```
+///
+/// # Example
+///
+/// ```rust,ignore
+/// use mingling::macros::{chain, empty_result};
+///
+/// #[chain]
+/// fn maybe_skip(prev: SomeEntry) -> Next {
+/// if should_skip() {
+/// return empty_result!();
+/// }
+/// // ... continue processing
+/// NextEntry::new(result).to_chain()
+/// }
+/// ```
+///
+/// # Generated code
+///
+/// The macro expands to:
+/// ```rust,ignore
+/// crate::EmptyResult::new(()).to_chain()
+/// ```
+///
+/// This works because `EmptyResult` is automatically generated by `gen_program!`
+/// and implements the necessary trait conversions into `ChainProcess`.
+#[proc_macro]
+pub fn empty_result(_input: TokenStream) -> TokenStream {
+ let expanded = quote! {
+ crate::EmptyResult::new(()).to_chain()
+ };
+ TokenStream::from(expanded)
+}
+
/// Creates a `Dispatcher` implementation for a subcommand.
///
/// This is the primary way to define command-line subcommands in Mingling.