diff options
| -rw-r--r-- | CHANGELOG.md | 18 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | examples/example-dispatch-tree/src/main.rs | 7 | ||||
| -rw-r--r-- | examples/example-exit-code/src/main.rs | 6 | ||||
| -rw-r--r-- | examples/example-repl/Cargo.lock | 92 | ||||
| -rw-r--r-- | examples/example-repl/Cargo.toml | 8 | ||||
| -rw-r--r-- | examples/example-repl/src/main.rs | 142 | ||||
| -rw-r--r-- | mingling/src/example_docs.rs | 173 | ||||
| -rw-r--r-- | mingling/src/lib.rs | 4 | ||||
| -rw-r--r-- | mingling_core/src/program.rs | 2 | ||||
| -rw-r--r-- | mingling_core/src/program/repl_exec.rs | 24 | ||||
| -rw-r--r-- | mingling_core/src/program/repl_exec/res.rs | 6 | ||||
| -rw-r--r-- | mingling_macros/src/lib.rs | 47 |
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 @@ -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 = ¤t_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 = ¤t_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. |
