diff options
Diffstat (limited to 'mingling')
| -rw-r--r-- | mingling/src/example_docs.rs | 1616 | ||||
| -rw-r--r-- | mingling/src/lib.rs | 4 |
2 files changed, 1284 insertions, 336 deletions
diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs index bb65280..ce65a84 100644 --- a/mingling/src/example_docs.rs +++ b/mingling/src/example_docs.rs @@ -1,83 +1,228 @@ // Auto generated -/// `Mingling` Example - Async +/// Example Argument Parse /// -/// After enabling the `async` feature: -/// 1. The `chain!` macro will support using **async** functions, -/// 2. The `exec` function of `Program` will return a `Future` for you to use with an async runtime +/// > This example demonstrates how to use the `parser` feature to parse user input /// -/// ## Enable Feature -/// Enable the `async` feature for mingling in `Cargo.toml` -/// ```toml -/// [dependencies] -/// mingling = { version = "...", features = ["async"] } +/// Run: +/// ```bash +/// cargo run --manifest-path examples/example-argument-parse/Cargo.toml --quiet -- transfer README.md --size 32kib +/// cargo run --manifest-path examples/example-argument-parse/Cargo.toml --quiet -- transfer src/ --dir +/// cargo run --manifest-path examples/example-argument-parse/Cargo.toml --quiet -- strict-transfer README.md +/// cargo run --manifest-path examples/example-argument-parse/Cargo.toml --quiet -- strict-transfer --dir /// ``` /// -/// # How to Run -/// ```bash -/// cargo run --manifest-path ./examples/example-async/Cargo.toml -- hello World +/// Output: +/// ```plaintext +/// file: README.md (32768) +/// dir: src/ (1048576) +/// file: README.md (1048576) +/// Error: name is not provided /// ``` /// /// Cargo.toml /// ```ignore /// [package] -/// name = "example-async" -/// version = "0.0.1" +/// name = "example-argument-parse" +/// version = "0.1.0" /// edition = "2024" /// -/// [dependencies] -/// tokio = { version = "1", features = ["full"] } -/// mingling = { path = "../../mingling", features = ["async"] } +/// [dependencies.mingling] +/// path = "../../mingling" +/// +/// # Enable `parser` features +/// features = ["parser"] /// ``` /// /// main.rs /// ```ignore -/// use mingling::prelude::*; +/// use mingling::{macros::route, prelude::*}; +/// +/// dispatcher!("transfer", CMDTransfer => EntryTransfer); +/// dispatcher!("strict-transfer", CMDStrictTransfer => EntryStrictTransfer); +/// +/// pack!(ResultFile = (bool, usize, String)); // (IsDir, Size, Name) +/// +/// #[chain] +/// fn handle_transfer_parse(args: EntryTransfer) -> Next { +/// // --------- IMPORTANT --------- +/// // First parse flag arguments (like --dir/-D), then positional arguments +/// let result: ResultFile = args +/// // Name --dir --size 20mib +/// // ^^^^^^^^^^^^_ first +/// .pick::<bool>(["--dir", "-D"]) +/// // Name --dir +/// // ^^^^^_ second (or `-D`) +/// .pick_or::<usize>("--size", 1024 * 1024_usize) +/// // Name +/// // ^^^^_ finally, pick positional arg +/// .pick::<String>(()) +/// .after(|str| str.trim().replace(" ", "")) +/// // Unpack to tuple (is_dir, size, name) +/// .unpack() +/// // Convert into ResultFile +/// .into(); +/// // --------- IMPORTANT --------- +/// result +/// } +/// +/// pack!(ErrorNoNameProvided = ()); +/// +/// #[chain] +/// fn handle_strict_transfer_parse(args: EntryStrictTransfer) -> Next { +/// // --------- IMPORTANT --------- +/// // Strict parsing: error immediately if the name is not provided +/// let result: ResultFile = route! { // Use `route!` to wrap a Picker that contains `or_route` +/// args +/// .pick::<bool>(["--dir", "-D"]) +/// .pick_or::<usize>("--size", 1024 * 1024_usize) +/// // Finally parse the positional argument; if not found, route to `ErrorNoNameProvided` +/// .pick_or_route::<String, _>((), ErrorNoNameProvided::default().to_chain()) +/// .after(|str| str.trim().replace(" ", "")) +/// .unpack() +/// } +/// // Convert into ResultFile +/// .into(); +/// // --------- IMPORTANT --------- +/// result.to_chain() +/// } +/// +/// #[renderer] +/// fn render_result_file(result: ResultFile) { +/// let (is_dir, size, name) = result.into(); +/// r_println!( +/// "{}: {} ({})", +/// if is_dir { "dir" } else { "file" }, +/// name, +/// size +/// ) +/// } +/// +/// #[renderer] +/// fn render_error_no_name_provided(_: ErrorNoNameProvided) { +/// r_println!("Error: name is not provided") +/// } +/// +/// gen_program!(); +/// +/// fn main() { +/// let mut program = ThisProgram::new(); +/// program.with_dispatcher(CMDTransfer); +/// program.with_dispatcher(CMDStrictTransfer); +/// program.exec_and_exit(); +/// } +/// ``` +pub mod example_argument_parse {} +/// Example Async Runtime Support /// -/// dispatcher!("hello", HelloCommand => HelloEntry); +/// > This example shows how to drive an async runtime using the `async` feature +/// +/// ## Note +/// +/// When the `async` feature is enabled, **Mingling** provides a different framework implementation, +/// allowing you to use the `async` keyword directly within `#[chain]`. +/// +/// However, you will lose some capabilities: +/// +/// 1. `&mut` resource injection is not available in async chain functions +/// 2. The program will not be able to use panic unwind functionality +/// +/// Run: +/// ```bash +/// cargo run --manifest-path examples/example-async-support/Cargo.toml --quiet -- download README.md +/// ``` +/// +/// Output: +/// ```plaintext +/// Download begin +/// # (Will pause for 1 second here) +/// "README.md" downloaded. +/// ``` +/// +/// Cargo.toml +/// ```ignore +/// [package] +/// name = "example-async-support" +/// version = "0.1.0" +/// edition = "2024" +/// +/// [dependencies.mingling] +/// path = "../../mingling" +/// +/// # Enable `parser` features +/// features = ["async", "parser"] +/// +/// # Import any async runtime, e.g. Tokio +/// [dependencies.tokio] +/// version = "1.52.3" +/// features = ["macros", "rt", "rt-multi-thread", "time"] +/// ``` +/// +/// main.rs +/// ```ignore +/// use mingling::{hook::ProgramHook, prelude::*}; /// -/// // Use Tokio async runtime /// #[tokio::main] /// async fn main() { /// let mut program = ThisProgram::new(); -/// program.with_dispatcher(HelloCommand); /// -/// // Run program -/// program.exec().await; +/// program.with_dispatcher(CMDDownload); +/// +/// // Add a hook to display when the download begins +/// program.with_hook(ProgramHook::empty().on_begin(|| println!("Download begin"))); +/// +/// // --------- IMPORTANT --------- +/// // The return values of `exec_*()` related functions have been replaced with Futures +/// program.exec_and_exit().await; +/// // --------- IMPORTANT --------- /// } /// -/// pack!(Hello = String); +/// dispatcher!("download", CMDDownload => EntryDownload); /// -/// // You can freely use async / non-async functions to declare your Chain +/// pack!(ResultDownloaded = String); /// +/// // --------- IMPORTANT --------- /// #[chain] -/// // fn parse_name(prev: HelloEntry) -> Next { -/// async fn parse_name(prev: HelloEntry) -> Next { -/// let name = prev.first().cloned().unwrap_or_else(|| "World".to_string()); -/// Hello::new(name).to_render() +/// // vvvvv_ `async` keyword can be used directly here +/// pub async fn handle_download(args: EntryDownload) -> Next { +/// let file_name = args.pick(()).unpack(); +/// fake_download(file_name).await /// } /// -/// // For renderers, you can still only use synchronous functions /// #[renderer] -/// fn render_hello_who(prev: Hello) { -/// r_println!("Hello, {}!", *prev); +/// // But renderers cannot use the `async` keyword +/// pub fn render_downloaded(result: ResultDownloaded) { +/// r_println!("\"{}\" downloaded.", *result); /// } +/// // --------- IMPORTANT --------- /// /// gen_program!(); +/// +/// async fn fake_download(file_name: String) -> ResultDownloaded { +/// tokio::time::sleep(std::time::Duration::from_secs(1)).await; +/// ResultDownloaded::new(file_name) +/// } /// ``` -pub mod example_async {} -/// `Mingling` Example - Basic +pub mod example_async_support {} +/// Example The Basic Usage of Mingling /// -/// # How to Run -/// ```bash -/// cargo run --manifest-path ./examples/example-basic/Cargo.toml -- hello World +/// Run: +/// ```base +/// cargo run --manifest-path examples/example-basic/Cargo.toml --quiet -- greet +/// cargo run --manifest-path examples/example-basic/Cargo.toml --quiet -- greet Alice +/// ``` +/// +/// Output: +/// ```plaintext +/// Hello, World! +/// Hello, Alice! /// ``` /// /// Cargo.toml /// ```ignore /// [package] /// name = "example-basic" -/// version = "0.0.1" +/// version = "0.1.0" /// edition = "2024" /// /// [dependencies] @@ -86,215 +231,389 @@ pub mod example_async {} /// /// main.rs /// ```ignore +/// // Import commonly used Mingling modules /// use mingling::prelude::*; /// -/// // Define dispatcher `HelloCommand`, directing subcommand "hello" to `HelloEntry` -/// dispatcher!("hello", HelloCommand => HelloEntry); +/// // Define the `greet` subcommand +/// // _____________________________ subcmd name, can be nested (e.g. "remote.add" "remote.rm") +/// // / _____________________ dispatcher name +/// // | / _________ entry, records raw arguments +/// // | | / ^^^^^^^^^^^^^ +/// // vvvvv vvvvvvvv vvvvvvvvvv \_ equivalent to pack!(EntryGreet = Vec<String>) +/// dispatcher!("greet", CMDGreet => EntryGreet); /// /// fn main() { -/// // Create program +/// // Create a new ThisProgram /// let mut program = ThisProgram::new(); /// -/// // Add dispatcher `HelloCommand` -/// program.with_dispatcher(HelloCommand); +/// // Add the CMDGreet dispatcher +/// program.with_dispatcher(CMDGreet); /// -/// // Run program -/// program.exec(); +/// // Run the program, then exit the process +/// program.exec_and_exit(); /// } /// -/// // Register wrapper type `Hello`, setting inner to `String` -/// pack!(Hello = String); -/// -/// // Register chain to `ThisProgram`, handling logic from `HelloEntry` -/// #[chain] -/// fn parse_name(prev: HelloEntry) -> Next { -/// // Extract string from `HelloEntry` as argument -/// let name = prev.first().cloned().unwrap_or_else(|| "World".to_string()); -/// -/// // Build `Hello` type and route to renderer -/// Hello::new(name).to_render() +/// // Quickly wrap a type into a type recognizable by the current program +/// // ____________________ Wrapped type name +/// // / _______ Wrapped type inner value +/// // | / +/// // vvvvvvvvvv vvvvvv +/// pack!(ResultName = String); +/// +/// // Define the `handle_greet` chain for parsing input text +/// // ____________________ Previous type: +/// // / Mingling deduces types at runtime and routes them to this function +/// // | _____ will be expanded to: +/// // | / impl Into<mingling::ChainProcess<ThisProgram>> +/// #[chain] // vvvvvvvvvv vvvv +/// fn handle_greet(args: EntryGreet) -> Next { +/// let name: ResultName = args +/// .inner +/// .first() +/// .cloned() +/// .unwrap_or_else(|| "World".to_string()) +/// .into(); +/// name /// } /// -/// // Register renderer to `ThisProgram`, handling rendering of `Hello` +/// // Define renderer `render_name`, used to render `ResultName` /// #[renderer] -/// fn render_hello_who(prev: Hello) { -/// // Print message -/// r_println!("Hello, {}!", *prev); -/// -/// // Program ends here +/// fn render_name(name: ResultName) { +/// r_println!("Hello, {}!", *name); /// } /// -/// // Generate program, default is `ThisProgram` +/// // Note: This macro generates the program entry point. +/// // It must be placed at the end of the root module of the crate (>= mingling@0.1.8). +/// // ^^^^^^ ^^^^^^^^^^^ +/// // For example: lib.rs, main.rs /// gen_program!(); /// ``` pub mod example_basic {} -/// `Mingling` Example - Completion -/// -/// # How to Deploy -/// 1. Enable the `comp` feature -/// ```toml -/// [dependencies] -/// mingling = { version = "...", features = [ -/// "comp", // Enable this feature -/// "parser" -/// ] } -/// ``` +/// Example Completion /// -/// 2. Add `mingling` as a build dependency, enabling the `builds` and `comp` features -/// ```toml -/// [build-dependencies] -/// mingling = { version = "...", features = [ -/// "builds", // Enable this feature for build scripts -/// "comp" -/// ] } -/// ``` +/// > This example demonstrates how to use **Mingling** to create fully dynamic command-line completions +/// +/// ## About Completion Scripts /// -/// 3. Write `build.rs` to generate completion scripts at compile time -/// ```ignore -/// use mingling::build::{build_comp_scripts, build_comp_scripts_with_bin_name}; +/// To make your completions work, you need to generate a completion script using Mingling's tools +/// +/// 1. Enable features +/// You need to enable the `builds` and `comp` features for `mingling` in `[build-dependencies]` +/// +/// 2. Write `build.rs` +/// Write the following in `build.rs` +/// +/// ```rust,ignore /// fn main() { -/// // Generate completion scripts for the current program, using the Cargo package name as the binary filename -/// build_comp_scripts(env!("CARGO_PKG_NAME")).unwrap(); +/// build_scripts(); +/// } /// -/// // Or, explicitly specify the binary filename -/// // build_comp_scripts("your_bin").unwrap(); +/// /// Generate completion scripts +/// fn build_scripts() { +/// // `env!("CARGO_PKG_NAME")` equals the crate name, which matches the binary name. +/// // If your binary name differs from the crate name, specify it explicitly. +/// mingling::build::build_comp_scripts( +/// // Your binary name: +/// env!("CARGO_PKG_NAME"), +/// ) +/// .unwrap(); /// } /// ``` /// -/// 4. Write `main.rs`, adding completion logic for your command entry point -/// 5. Execute `cargo install --path ./`, then run the corresponding completion script in your shell +/// 3. Verify +/// Build your project with `cargo build --release`. The completion scripts will be generated in `target/release/` +/// +/// Execute the script or have it be automatically sourced by your Shell +/// +/// Run: +/// ```bash +/// cargo run --manifest-path examples/example-completion/Cargo.toml --quiet -- greet Alice --repeat 3 +/// ``` +/// +/// Output: +/// ```plaintext +/// Hello, Alice, Alice, Alice! +/// ``` /// /// Cargo.toml /// ```ignore /// [package] /// name = "example-completion" -/// version = "0.0.1" +/// version = "0.1.0" /// edition = "2024" /// -/// [dependencies] -/// mingling = { path = "../../mingling", features = ["comp", "parser"] } +/// [dependencies.mingling] +/// path = "../../mingling" +/// +/// features = [ +/// # Enable `comp` features +/// "comp", +/// "parser", +/// ] +/// +/// [build-dependencies.mingling] +/// path = "../../mingling" +/// +/// features = [ +/// # Enable `comp` features +/// "comp", +/// +/// # If you want to build completion scripts, +/// # enable `builds` features +/// "builds", +/// ] /// ``` /// /// main.rs /// ```ignore -/// use mingling::prelude::*; -/// use mingling::{ -/// macros::{suggest, suggest_enum}, -/// parser::{PickableEnum, Picker}, -/// EnumTag, Groupped, ShellContext, Suggest, -/// }; +/// use mingling::{macros::suggest, prelude::*, ShellContext, Suggest}; +/// +/// fn main() { +/// let mut program = ThisProgram::new(); /// -/// // Define dispatcher `FruitCommand`, directing subcommand "fruit" to `FruitEntry` -/// dispatcher!("fruit", FruitCommand => FruitEntry); +/// program.with_dispatcher(CMDGreet); /// -/// #[completion(FruitEntry)] -/// fn comp_fruit_command(ctx: &ShellContext) -> Suggest { -/// if ctx.filling_argument_first("--name") { -/// return suggest!(); +/// // --------- IMPORTANT --------- +/// // The `comp` feature makes `gen_program!()` generate a CompletionDispatcher automatically +/// // It adds a hidden `__comp` subcommand for communication with the completion script +/// program.with_dispatcher(crate::CompletionDispatcher); +/// // --------- IMPORTANT --------- +/// +/// // TIP: Note that the completion script reads stdout, +/// // so make sure no output is produced before the CompletionDispatcher is dispatched. +/// program.exec_and_exit(); +/// } +/// +/// // --------- IMPORTANT --------- +/// // __________________________________________ Entry point bound to completion behavior +/// // / _________________________ Shell context for obtaining user input state +/// // | / ________ Suggest, used to return completion results +/// // vvvvvvvvvv | / +/// #[completion(EntryGreet)] // vvvvvvvvvvvv vvvvvvv +/// fn complete_greet_entry(ctx: &ShellContext) -> Suggest { +/// // When the previous word is `greet` (the current command being typed) +/// if ctx.previous_word == "greet" { +/// // Return suggestions +/// return suggest! { +/// "Bob": "Likes to pass messages", +/// "Alice": "Likes to receive messages", +/// "Hacker": "YOU", +/// "World" +/// }; /// } -/// if ctx.filling_argument_first("--type") { -/// return suggest_enum!(FruitType); +/// +/// // When the user is typing `--repeat` +/// if ctx.filling_argument(["-r", "--repeat"]) { +/// return suggest! {}; // Don't suggest anything /// } +/// +/// // When the user is typing `-` /// if ctx.typing_argument() { /// return suggest! { -/// "--name": "Fruit name", -/// "--type": "Fruit type" +/// "-r": "Number of repetitions", +/// "--repeat": "Number of repetitions", /// } +/// // Remove arguments that have already been typed by the user /// .strip_typed_argument(ctx); /// } -/// return suggest!(); +/// +/// // Otherwise, suggest nothing +/// suggest!() +/// // // You can also enable file completions using the following code, +/// // // which will invoke the Shell's default behavior +/// // Suggest::file_comp() /// } +/// // --------- IMPORTANT --------- /// -/// fn main() { -/// let mut program = ThisProgram::new(); -/// program.with_dispatcher(CompletionDispatcher); -/// program.with_dispatcher(FruitCommand); -/// program.exec(); +/// dispatcher!("greet", CMDGreet => EntryGreet); +/// pack!(ResultName = (u8, String)); +/// +/// #[chain] +/// fn handle_greet(args: EntryGreet) -> Next { +/// let result: ResultName = args +/// .pick(["-r", "--repeat"]) +/// .pick_or((), "World") +/// .unpack() +/// .into(); +/// result /// } /// -/// #[derive(Groupped)] -/// struct FruitInfo { -/// name: String, -/// fruit_type: FruitType, +/// #[renderer] +/// fn render_name(result: ResultName) { +/// let (repeat, name) = result.inner; +/// let mut parts = Vec::with_capacity(repeat as usize); +/// for _ in 0..repeat { +/// parts.push(name.clone()); +/// } +/// r_println!("Hello, {}!", parts.join(", ")); /// } /// -/// #[derive(Default, Debug, EnumTag)] -/// enum FruitType { -/// #[enum_desc("It's Apple")] -/// #[enum_rename("apple")] -/// FruitApple, +/// gen_program!(); +/// ``` +pub mod example_completion {} +/// Example Custom Pickable /// -/// #[enum_desc("It's Banana")] -/// #[enum_rename("banana")] -/// FruitBanana, +/// > This example demonstrates how to use the Pickable trait to add parsing for your types /// -/// #[enum_desc("It's Cherry")] -/// #[enum_rename("cherry")] -/// FruitCherry, +/// Run: +/// ```bash +/// cargo run --manifest-path examples/example-custom-pickable/Cargo.toml --quiet -- connect 127.0.0.1:5012 +/// cargo run --manifest-path examples/example-custom-pickable/Cargo.toml --quiet -- connect 127.0.0.1 +/// ``` /// -/// #[enum_desc("It's Date")] -/// #[enum_rename("date")] -/// FruitDate, +/// Output: +/// ```plaintext +/// Connected to "127.0.0.1:5012" +/// Failed to parse address +/// ``` /// -/// #[enum_desc("It's Elderberry")] -/// #[enum_rename("elderberry")] -/// FruitElderberry, +/// Cargo.toml +/// ```ignore +/// [package] +/// name = "example-custom-pickable" +/// version = "0.1.0" +/// edition = "2024" /// -/// #[default] -/// #[enum_rename("unknown")] -/// Unknown, +/// [dependencies.mingling] +/// path = "../../mingling" +/// +/// features = [ +/// "parser", +/// ] +/// ``` +/// +/// main.rs +/// ```ignore +/// use mingling::{Groupped, macros::route, parser::Pickable, prelude::*}; +/// +/// // Define types that can be recognized by Mingling +/// // ________________________ `Pickable` trait needs to implement Default +/// // / ________ The Groupped derive macro registers an ID for this type +/// // | / Mingling uses this ID to identify the type +/// // vvvvvvv vvvvvvvv +/// #[derive(Debug, Default, Clone, Groupped)] +/// pub struct Address { +/// pub ip: [u8; 4], +/// pub port: u16, +/// } +/// +/// // --------- IMPORTANT --------- +/// impl Pickable for Address { +/// type Output = Address; +/// fn pick(args: &mut mingling::parser::Argument, flag: mingling::Flag) -> Option<Self::Output> { +/// // Extract the raw string from Argument using the Flag +/// let raw: String = args.pick_argument(flag)?.to_string(); +/// +/// // Use TryFrom to parse the address +/// Address::try_from(raw).ok() +/// } /// } +/// // --------- IMPORTANT --------- /// -/// impl PickableEnum for FruitType {} +/// dispatcher!("connect", CMDConnect => EntryConnect); +/// pack!(ErrorParseAddressFailed = ()); /// /// #[chain] -/// fn parse_fruit_info(prev: FruitEntry) -> Next { -/// let picker = Picker::from(prev.inner); -/// let (fruit_name, fruit_type) = picker.pick("--name").pick("--type").unpack(); -/// let info = FruitInfo { -/// name: fruit_name, -/// fruit_type, -/// }; -/// info.to_render() +/// fn handle_connect(prev: EntryConnect) -> Next { +/// let connect: Address = +/// route! { prev.pick_or_route((), ErrorParseAddressFailed::default().to_chain()).unpack() }; +/// connect.to_chain() /// } /// /// #[renderer] -/// fn render_fruit(prev: FruitInfo) { -/// match (prev.name.is_empty(), prev.fruit_type) { -/// (true, FruitType::Unknown) => { -/// r_println!("Fruit name is empty and type is unknown"); -/// } -/// (true, fruit_type) => { -/// r_println!("Fruit name is empty, Type: {:?}", fruit_type); +/// fn render_address(addr: Address) { +/// r_println!("Connected to \"{}\"", addr.to_string()); +/// } +/// +/// #[renderer] +/// fn render_error_parse_address_failed(_: ErrorParseAddressFailed) { +/// r_println!("Failed to parse address"); +/// } +/// +/// gen_program!(); +/// +/// fn main() { +/// let mut program = ThisProgram::new(); +/// program.with_dispatcher(CMDConnect); +/// program.exec_and_exit(); +/// } +/// +/// // Address conversion +/// +/// impl TryFrom<String> for Address { +/// type Error = String; +/// +/// fn try_from(raw: String) -> Result<Self, Self::Error> { +/// // Expected format: "192.168.1.1:8080" +/// let parts: Vec<&str> = raw.split(':').collect(); +/// if parts.len() != 2 { +/// return Err("Invalid format: expected 'IP:PORT'".to_string()); /// } -/// (false, FruitType::Unknown) => { -/// r_println!("Fruit name: {}, Type is unknown", prev.name); +/// +/// let ip_str = parts[0]; +/// let port_str = parts[1]; +/// +/// // Parse IP address (4 octets separated by dots) +/// let ip_parts: Vec<&str> = ip_str.split('.').collect(); +/// if ip_parts.len() != 4 { +/// return Err("Invalid IP address format".to_string()); /// } -/// (false, fruit_type) => { -/// r_println!("Fruit name: {}, Type: {:?}", prev.name, fruit_type); +/// +/// let mut ip = [0u8; 4]; +/// for (i, part) in ip_parts.iter().enumerate() { +/// ip[i] = part +/// .parse::<u8>() +/// .map_err(|_| format!("Invalid IP octet: {}", part))?; /// } +/// +/// // Parse port +/// let port = port_str +/// .parse::<u16>() +/// .map_err(|_| format!("Invalid port: {}", port_str))?; +/// +/// Ok(Address { ip, port }) /// } /// } /// -/// gen_program!(); +/// impl From<Address> for String { +/// fn from(addr: Address) -> String { +/// format!( +/// "{}.{}.{}.{}:{}", +/// addr.ip[0], addr.ip[1], addr.ip[2], addr.ip[3], addr.port +/// ) +/// } +/// } +/// +/// impl std::fmt::Display for Address { +/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +/// write!( +/// f, +/// "{}.{}.{}.{}:{}", +/// self.ip[0], self.ip[1], self.ip[2], self.ip[3], self.port +/// ) +/// } +/// } /// ``` -pub mod example_completion {} -/// `Mingling` Example - Dispatch Tree -/// -/// # How to Deploy -/// 1. Enable the `dispatch_tree` feature (`comp` is optional) -/// ```toml -/// mingling = { version = "...", features = [ -/// "dispatch_tree", // Enable this feature -/// "comp" // optional -/// ] } -/// ``` +pub mod example_custom_pickable {} +/// Example Dispatch Tree +/// +/// > This example will introduce how to use `dispatch_tree` +/// > to optimize your command line lookup efficiency /// -/// 2. Using `cargo expand`: +/// When the number of commands in your project increases, you can use `dispatch_tree` to complete command registration at compile time. +/// It will generate a trie for quickly finding related commands by prefix. /// +/// Therefore, after enabling this feature, +/// `Program` will no longer store a Dispatcher list internally, and the `with_dispatcher` function will not be compiled. +/// +/// Run: /// ```bash -/// cargo expand --manifest-path examples/example-dispatch-tree/Cargo.toml > expanded.rs -/// cat expanded.rs +/// cargo run --manifest-path examples/example-dispatch-tree/Cargo.toml --quiet -- cmd5 +/// ``` +/// +/// Output: +/// ```plaintext +/// It's works! /// ``` /// /// Cargo.toml @@ -304,123 +623,378 @@ pub mod example_completion {} /// version = "0.1.0" /// edition = "2024" /// -/// [dependencies] -/// mingling = { path = "../../mingling", features = ["dispatch_tree", "comp"] } +/// [dependencies.mingling] +/// path = "../../mingling" +/// +/// features = [ +/// "dispatch_tree", +/// ] /// ``` /// /// main.rs /// ```ignore -/// #![allow(unused_mut)] -/// /// use mingling::prelude::*; /// +/// // --------- IMPORTANT --------- +/// // You have a large number of subcommands +/// dispatcher!("cmd1", CMD1 => Entry1); +/// dispatcher!("cmd2.sub1", CMD2Sub1 => Entry2Sub1); +/// dispatcher!("cmd2.sub2", CMD2Sub2 => Entry2Sub2); +/// dispatcher!("cmd3.sub1.leaf1", CMD3Sub1Leaf1 => Entry3Sub1Leaf1); +/// dispatcher!("cmd3.sub1.leaf2", CMD3Sub1Leaf2 => Entry3Sub1Leaf2); +/// dispatcher!("cmd3.sub2", CMD3Sub2 => Entry3Sub2); +/// dispatcher!("cmd4.sub1.subsub1.deep", CMD4Deep => Entry4Deep); +/// dispatcher!("cmd4.sub1.subsub2", CMD4SubSub2 => Entry4SubSub2); +/// dispatcher!("cmd5", CMD5 => Entry5); +/// dispatcher!("cmd5.extra", CMD5Extra => Entry5Extra); +/// dispatcher!("nested.a.b.c", CMDA => EntryA); +/// dispatcher!("nested.a.b.d", CMDB => EntryB); +/// dispatcher!("nested.a.e", CMDC => EntryC); +/// dispatcher!("nested.f", CMDD => EntryD); +/// // --------- IMPORTANT --------- +/// /// fn main() { -/// let mut program = ThisProgram::new(); +/// let program = ThisProgram::new(); /// -/// // // 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 -/// // program.with_dispatcher(CompletionDispatcher); +/// // --------- IMPORTANT --------- +/// // // You no longer need to use `with_dispatcher` anymore; +/// // // it'll be collected automatically once the `dispatch_tree` feature is enabled +/// // program.with_dispatcher(...); /// -/// program.exec(); +/// program.exec_and_exit() /// } /// -/// dispatcher!("greet", CommandGreet => EntryGreet); -/// dispatcher!("help", CommandHelp => EntryHelp); -/// dispatcher!("quit", CommandQuit => EntryQuit); -/// dispatcher!("list", CommandList => EntryList); -/// dispatcher!("status", CommandStatus => EntryStatus); -/// dispatcher!("save", CommandSave => EntrySave); -/// dispatcher!("load", CommandLoad => EntryLoad); -/// dispatcher!("config", CommandConfig => EntryConfig); -/// dispatcher!("run", CommandRun => EntryRun); -/// dispatcher!("debug", CommandDebug => EntryDebug); -/// dispatcher!("version", CommandVersion => EntryVersion); +/// #[renderer] +/// fn render_cmd5(_: Entry5) { +/// r_println!("It's works!"); +/// } /// /// gen_program!(); /// ``` pub mod example_dispatch_tree {} -/// `Mingling` Example - Exit Code +/// Example Enum Tag /// -/// This example demonstrates how to modify the program's exit code using `ExitCodeSetup`. -/// By default, the program exits with code 0. This example shows: -/// 1. Using `dispatcher!` to define an error command, -/// 2. Using `chain!` to handle errors and set a custom exit code via `ProgramExitCode`, -/// 3. Using `renderer!` to print an error message. +/// > This example demonstrates how to use the `EnumTag` derive macro to tag enum variants with metadata, +/// > which can be used for autocompletion and parsing /// -/// # How to Run +/// Run: /// ```bash -/// cargo run --manifest-path ./examples/example-exit-code/Cargo.toml -- error +/// cargo run --manifest-path examples/example-enum-tag/Cargo.toml --quiet -- lang-select OCaml +/// cargo run --manifest-path examples/example-enum-tag/Cargo.toml --quiet -- lang-select +/// ``` +/// +/// Output: +/// ```plaintext +/// Selected: OCaml (A representative functional programming language with strong type inference) +/// Selected: Rust (A systems programming language focused on performance, safety, and concurrency) /// ``` /// /// Cargo.toml /// ```ignore /// [package] -/// name = "example-exit-code" +/// name = "example-enum-tag" /// version = "0.1.0" /// edition = "2024" /// -/// [dependencies] -/// mingling = { path = "../../mingling" } +/// [dependencies.mingling] +/// path = "../../mingling" +/// +/// features = [ +/// "comp", +/// "parser" +/// ] /// ``` /// /// main.rs /// ```ignore -/// use mingling::prelude::*; -/// use mingling::{res::ExitCode, setup::ExitCodeSetup}; +/// use mingling::{ +/// EnumTag, Groupped, ShellContext, Suggest, macros::suggest_enum, parser::PickableEnum, +/// prelude::*, +/// }; +/// +/// // Define the enum and derive the EnumTag trait +/// // ________ adds metadata to the enum, enabling it to: +/// // / 1. Be used by the `suggest_enum!(Enum)` macro under the `comp` feature for autocompletion +/// // vvvvvvv 2. Implement the `PickableEnum` trait +/// #[derive(Debug, Default, EnumTag, Groupped)] +/// pub enum ProgrammingLanguages { +/// #[enum_desc("An efficient and flexible compiled language widely used for system programming")] +/// C, +/// +/// #[enum_rename("C++")] +/// #[enum_desc("A high-performance language extending C with object-oriented features")] +/// CPlusPlus, +/// +/// #[enum_rename("C#")] +/// #[enum_desc("Microsoft's object-oriented programming language running on the .NET platform")] +/// Csharp, +/// +/// #[enum_desc( +/// "A cross-platform object-oriented language widely used for enterprise application development" +/// )] +/// Java, +/// +/// #[enum_desc( +/// "A dynamic scripting language for web development, supporting prototype chain inheritance" +/// )] +/// JavaScript, +/// +/// #[enum_desc("A modern statically typed language running on the JVM, concise and safe")] +/// Kotlin, +/// +/// #[enum_desc("A representative functional programming language with strong type inference")] +/// OCaml, +/// +/// #[enum_desc("A general-purpose programming language with clean syntax, known for readability")] +/// Python, +/// +/// #[enum_desc("An object-oriented scripting language, famous for its concise and elegant syntax")] +/// Ruby, +/// +/// #[default] +/// #[enum_desc("A systems programming language focused on performance, safety, and concurrency")] +/// Rust, +/// } +/// +/// // --------- IMPORTANT --------- +/// // Implement the PickableEnum trait for ProgrammingLanguages, +/// // so that `Picker` can parse this enum +/// impl PickableEnum for ProgrammingLanguages {} +/// // --------- IMPORTANT --------- +/// +/// dispatcher!("lang-select", CMDLanguageSelection => EntryLanguageSelection); +/// +/// #[chain] +/// fn handle_language_selection(args: EntryLanguageSelection) -> Next { +/// // You can use Picker to directly parse ProgrammingLanguages +/// let lang: ProgrammingLanguages = args.pick(()).unpack(); +/// lang +/// } +/// +/// #[renderer] +/// fn render_programming_language(lang: ProgrammingLanguages) { +/// // You can use `enum_info()` to get the name and description of the current enum +/// let (name, desc) = lang.enum_info(); +/// r_println!("Selected: {} ({})", name, desc) +/// } +/// +/// #[completion(EntryLanguageSelection)] +/// fn complete_language_selection(_: &ShellContext) -> Suggest { +/// // Use `suggest_enum!` directly to generate enum suggestions +/// suggest_enum!(ProgrammingLanguages) +/// } +/// +/// gen_program!(); /// /// fn main() { /// let mut program = ThisProgram::new(); -/// program.with_dispatcher(ErrorCommand); -/// program.with_setup(ExitCodeSetup::<ThisProgram>::default()); +/// program.with_dispatcher(CompletionDispatcher); +/// program.with_dispatcher(CMDLanguageSelection); /// program.exec_and_exit(); /// } +/// ``` +pub mod example_enum_tag {} +/// Example Error Handling /// -/// dispatcher!("error", ErrorCommand => ErrorEntry); -/// pack!(ResultError = ()); +/// > This example demonstrates how to handle errors in Mingling, including custom error types and error rendering. +/// +/// Run: +/// ```bash +/// cargo run --manifest-path examples/example-error-handling/Cargo.toml --quiet -- hallo +/// cargo run --manifest-path examples/example-error-handling/Cargo.toml --quiet -- hello +/// cargo run --manifest-path examples/example-error-handling/Cargo.toml --quiet -- hello Alice +/// cargo run --manifest-path examples/example-error-handling/Cargo.toml --quiet -- hello MyBestFriendAlice +/// cargo run --manifest-path examples/example-error-handling/Cargo.toml --quiet -- hello Peter +/// ``` +/// +/// Output: +/// ```plaintext +/// Command not found: "hallo" +/// No name provided +/// Name not available +/// Name too long: 17 > 10 +/// Hello, Peter +/// ``` +/// +/// Cargo.toml +/// ```ignore +/// [package] +/// name = "example-error-handling" +/// version = "0.1.0" +/// edition = "2024" +/// +/// [dependencies] +/// mingling = { path = "../../mingling" } +/// ``` +/// +/// main.rs +/// ```ignore +/// use mingling::prelude::*; +/// +/// // In Mingling, instead of using ? to propagate errors upward, +/// // errors are treated as branches that continue execution. +/// +/// dispatcher!("hello", CMDHello => EntryHello); +/// +/// // Define error types +/// pack!(ErrorNoNameProvided = ()); +/// pack!(ErrorNameTooLong = u16); +/// pack!(ErrorNameNotAvailable = ()); +/// +/// // Define success type +/// pack!(ResultName = String); +/// +/// // Pre-registered names +/// static VEC_REGISTERED_NAMES: &[&str] = &["Alice", "Bob", "Charlie", "David", "Eve"]; /// /// #[chain] -/// fn handle_error_entry(_prev: ErrorEntry, ec: &mut ExitCode) -> Next { -/// ec.exit_code = 1; -/// return ResultError::default(); +/// fn handle_hello(args: EntryHello) -> Next { +/// let Some(name) = args.inner.first().cloned() else { +/// // If no name is provided, pass ErrorNoNameProvided +/// return ErrorNoNameProvided::default().to_render(); +/// }; +/// +/// if name.len() > 10 { +/// // If the name is too long, pass ErrorNameTooLong +/// return ErrorNameTooLong::new(name.len() as u16).to_render(); +/// } +/// +/// if VEC_REGISTERED_NAMES.contains(&name.as_str()) { +/// // If the name already exists, pass ErrorNameNotAvailable +/// return ErrorNameNotAvailable::default().to_render(); +/// } +/// +/// // If the name is valid, pass ResultName +/// ResultName::new(name).to_render() +/// } +/// +/// #[renderer] +/// fn render_result_name(name: ResultName) { +/// r_println!("Hello, {}", *name); +/// } +/// +/// #[renderer] +/// fn render_error_no_name_provided(_: ErrorNoNameProvided) { +/// // Prompt when no name is provided +/// r_println!("No name provided"); /// } /// /// #[renderer] -/// fn render_error(_prev: ResultError, ec: &ExitCode) { -/// r_println!("Exit with exit code: {}", ec.exit_code); +/// fn render_error_name_not_available(_: ErrorNameNotAvailable) { +/// // Prompt when name is already taken +/// r_println!("Name not available"); +/// } +/// +/// #[renderer] +/// fn render_error_name_too_long(len: ErrorNameTooLong) { +/// // Prompt when name is too long, showing actual length +/// r_println!("Name too long: {} > 10", *len); +/// } +/// +/// #[renderer] +/// fn render_dispatcher_not_found(err: DispatcherNotFound) { +/// // Prompt when command is not found, showing the input command +/// r_println!("Command not found: \"{}\"", err.inner.join(" ")); /// } /// /// gen_program!(); +/// +/// fn main() { +/// let mut program = ThisProgram::new(); +/// program.with_dispatcher(CMDHello); +/// program.exec_and_exit(); +/// } /// ``` -pub mod example_exit_code {} -/// `Mingling` Example - General Renderer +pub mod example_error_handling {} +/// Example Error Handling +/// +/// > This example demonstrates how to handle errors in Mingling, including custom error types and error rendering. /// -/// ## Step1 - Enable Feature -/// Enable the `general_renderer` feature for mingling in `Cargo.toml` -/// ```toml -/// [dependencies] -/// mingling = { version = "...", features = ["general_renderer", "parser"] } +/// Run: +/// ```bash +/// cargo run --manifest-path examples/example-exitcode/Cargo.toml --quiet -- hello Alice +/// cargo run --manifest-path examples/example-exitcode/Cargo.toml --quiet -- hello /// ``` /// -/// ## Step2 - Add Dependencies -/// Add `serde` dependency to `Cargo.toml` for serialization support -/// ```toml -/// [dependencies] -/// serde = { version = "1", features = ["derive"] } +/// Output: +/// ```plaintext +/// Hello, Alice +/// No name provided (with exit code 1) /// ``` /// -/// ## Step3 - Write Code -/// Write the following content into `main.rs` +/// Cargo.toml +/// ```ignore +/// [package] +/// name = "example-exitcode" +/// version = "0.1.0" +/// edition = "2024" +/// +/// [dependencies] +/// mingling = { path = "../../mingling" } +/// ``` /// -/// ## Step4 - Build and Run +/// main.rs +/// ```ignore +/// use mingling::{prelude::*, res::ExitCode, setup::ExitCodeSetup}; +/// +/// fn main() { +/// let mut program = ThisProgram::new(); +/// +/// // --------- IMPORTANT --------- +/// // Register `ExitCodeSetup` for the program to enable exit codes +/// program.with_setup(ExitCodeSetup::default()); +/// // --------- IMPORTANT --------- +/// +/// program.with_dispatcher(CMDHello); +/// program.exec_and_exit(); +/// } +/// +/// dispatcher!("hello", CMDHello => EntryHello); +/// +/// pack!(ErrorNoNameProvided = ()); +/// pack!(ResultName = String); +/// +/// #[chain] +/// fn handle_hello(args: EntryHello) -> Next { +/// let Some(name) = args.inner.first().cloned() else { +/// // If no name is provided, pass ErrorNoNameProvided +/// return ErrorNoNameProvided::default().to_render(); +/// }; +/// +/// // If the name is valid, pass ResultName +/// ResultName::new(name).to_render() +/// } +/// +/// #[renderer] +/// fn render_result_name(name: ResultName) { +/// r_println!("Hello, {}", *name); +/// } +/// +/// // Define renderer, render error message _____________ Inject exit code resource +/// // / +/// #[renderer] // vvvvvvvvvvvvv +/// fn render_error_no_name_provided(_: ErrorNoNameProvided, ec: &mut ExitCode) { +/// ec.exit_code = 1; +/// +/// // Prompt when no name is provided +/// r_println!("No name provided (with exit code 1)"); +/// } +/// +/// gen_program!(); +/// ``` +pub mod example_exitcode {} +/// Example General Renderer +/// +/// > This example demonstrates how to use the `general_renderer` feature to render data into structures such as json / yaml +/// +/// Run /// ```bash -/// cargo run --manifest-path ./examples/example-general-renderer/Cargo.toml -- render Bob 22 -/// cargo run --manifest-path ./examples/example-general-renderer/Cargo.toml -- render Bob 22 --json -/// cargo run --manifest-path ./examples/example-general-renderer/Cargo.toml -- render Bob 22 --yaml +/// cargo run --manifest-path examples/example-general-renderer/Cargo.toml --quiet -- render Bob 22 +/// cargo run --manifest-path examples/example-general-renderer/Cargo.toml --quiet -- render Bob 22 --json +/// cargo run --manifest-path examples/example-general-renderer/Cargo.toml --quiet -- render Bob 22 --yaml /// ``` /// -/// Will print: +/// Output: /// ```plain /// Bob is 22 years old /// {"member_name":"Bob","member_age":22} @@ -432,23 +1006,24 @@ pub mod example_exit_code {} /// ```ignore /// [package] /// name = "example-general-renderer" -/// version = "0.0.1" +/// version = "0.1.0" /// edition = "2024" /// /// [dependencies] -/// mingling = { path = "../../mingling", features = [ -/// "parser", +/// serde = { version = "1.0.228", features = ["derive"] } +/// +/// [dependencies.mingling] +/// path = "../../mingling" +/// features = [ /// "general_renderer", -/// "json_serde_fmt", -/// "yaml_serde_fmt", -/// ] } -/// serde = { version = "1", features = ["derive"] } +/// "parser", +/// ] /// ``` /// /// main.rs /// ```ignore /// use mingling::prelude::*; -/// use mingling::{parser::Picker, setup::GeneralRendererSetup, Groupped}; +/// use mingling::{Groupped, parser::Picker, setup::GeneralRendererSetup}; /// use serde::Serialize; /// /// dispatcher!("render", RenderCommand => RenderCommandEntry); @@ -461,7 +1036,13 @@ pub mod example_exit_code {} /// program.exec(); /// } /// -/// // Manually implement Info struct +/// // --------- IMPORTANT --------- +/// // For beautiful output structure, do not use `pack!` to wrap the types that need to be output. +/// // Instead, manually implement +/// // ____________________ Implement serde::Serialize +/// // / _________ Implement mingling::Groupped +/// // | / to ensure Mingling can recognize the type +/// // vvvvvvvvv vvvvvvvv /// #[derive(Serialize, Groupped)] /// struct Info { /// #[serde(rename = "member_name")] @@ -469,6 +1050,12 @@ pub mod example_exit_code {} /// #[serde(rename = "member_age")] /// age: i32, /// } +/// // This will output: {"member_name":"name","member_age":32} structure +/// +/// // If using pack!(Info = (String, i32)); +/// // Output: {"inner":["name", 32]} +/// +/// // --------- IMPORTANT --------- /// /// #[chain] /// fn parse_render(prev: RenderCommandEntry) -> Next { @@ -488,93 +1075,249 @@ pub mod example_exit_code {} /// gen_program!(); /// ``` pub mod example_general_renderer {} -/// `Mingling` Example - Picker +/// Example Help /// -/// ## Step1 - Enable Feature -/// Enable the `parser` feature for mingling in `Cargo.toml` -/// ```toml -/// [dependencies] -/// mingling = { version = "...", features = ["parser"] } +/// > This example demonstrates how to use the `#[help]` macro to generate help information, +/// > enabling `--help` to work +/// +/// Run +/// ```bash +/// cargo run --manifest-path examples/example-help/Cargo.toml --quiet -- greet --help /// ``` /// -/// ## Step2 - Write Code -/// Write the following content into `main.rs` +/// Output: +/// ```plain +/// Usage: greet <NAME> +/// ``` /// -/// ## Step3 - Build and Run -/// ```bash -/// cargo run --manifest-path ./examples/example-picker/Cargo.toml -- pick Bob -/// cargo run --manifest-path ./examples/example-picker/Cargo.toml -- pick Bob --age -15 -/// cargo run --manifest-path ./examples/example-picker/Cargo.toml -- pick --age 99 +/// Cargo.toml +/// ```ignore +/// [package] +/// name = "example-help" +/// version = "0.1.0" +/// edition = "2024" +/// +/// [dependencies] +/// mingling = { path = "../../mingling" } +/// ``` +/// +/// main.rs +/// ```ignore +/// use mingling::{macros::help, prelude::*, setup::BasicProgramSetup}; +/// +/// dispatcher!("greet", CMDGreet => EntryGreet); +/// +/// // Define help _________ When `program.user_context.help` is `true` +/// // / the command will not enter `#[chain]` / `#[renderer]` +/// #[help] // vvvvvvvvvv but instead enter this `#[help]` function +/// fn help_greet(_prev: EntryGreet) { +/// r_println!("Usage: greet <NAME>"); +/// } +/// +/// fn main() { +/// let mut program = ThisProgram::new(); +/// +/// // --------- IMPORTANT --------- +/// // Add `BasicProgramSetup` to the program +/// // to enable `--help`, `--quiet`, and other built-in features +/// program.with_setup(BasicProgramSetup); +/// // --------- IMPORTANT --------- +/// +/// program.with_dispatcher(CMDGreet); +/// +/// program.exec_and_exit(); +/// } +/// +/// gen_program!(); +/// ``` +pub mod example_help {} +/// Example Hook +/// +/// > This example demonstrates how to use Mingling's hook system to obtain debugging information during program execution +/// +/// Run: +/// ```base +/// cargo run --manifest-path examples/example-hook/Cargo.toml --quiet -- greet Alice +/// ``` +/// +/// Output: +/// ```plaintext +/// [DEBUG] Program is begin +/// [DEBUG] Pre dispatch: ["greet", "Alice"] +/// [DEBUG] Post dispatch: EntryGreet +/// [DEBUG] Pre chain: EntryGreet +/// [DEBUG] Post chain: ResultName +/// [DEBUG] Pre render: ResultName +/// [DEBUG] Post render +/// [DEBUG] Program end +/// Hello, Alice! /// ``` /// /// Cargo.toml /// ```ignore /// [package] -/// name = "example-picker" -/// version = "0.0.1" +/// name = "example-hook" +/// version = "0.1.0" /// edition = "2024" /// /// [dependencies] -/// mingling = { path = "../../mingling", features = ["parser"] } -/// tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } +/// mingling = { path = "../../mingling" } /// ``` /// /// main.rs /// ```ignore -/// use mingling::prelude::*; +/// use mingling::{hook::ProgramHook, prelude::*}; /// -/// dispatcher!("pick", PickCommand => PickEntry); +/// dispatcher!("greet", CMDGreet => EntryGreet); /// /// fn main() { /// let mut program = ThisProgram::new(); -/// program.with_dispatcher(PickCommand); -/// program.exec(); +/// +/// // --------- IMPORTANT --------- +/// program.with_hook( +/// ProgramHook::<ThisProgram>::empty() +/// .on_begin(|| println!("[DEBUG] Program is begin")) +/// .on_pre_dispatch(|args| println!("[DEBUG] Pre dispatch: {:?}", args)) +/// .on_post_dispatch(|c: &_| println!("[DEBUG] Post dispatch: {:?}", c)) +/// .on_pre_chain(|c: &_, _| { +/// println!("[DEBUG] Pre chain: {}", c); +/// }) +/// .on_post_chain(|any_output| println!("[DEBUG] Post chain: {}", any_output.member_id)) +/// .on_finish(|| { +/// println!("[DEBUG] Loop end"); +/// 0 // Override exit code +/// }) +/// .on_pre_render(|c: &_, _| println!("[DEBUG] Pre render: {}", c)) +/// .on_post_render(|_| println!("[DEBUG] Post render")), +/// ); +/// // --------- IMPORTANT --------- +/// +/// program.with_dispatcher(CMDGreet); +/// program.exec_and_exit(); /// } /// -/// pack!(NoNameProvided = ()); -/// pack!(ParsedPickInput = (i32, String)); +/// pack!(ResultName = String); /// /// #[chain] -/// fn parse(prev: PickEntry) -> Next { -/// let picked = prev -/// // First extract the named argument -/// .pick_or("--age", 20) -/// .after(|n: i32| n.clamp(0, 100)) -/// // Then sequentially extract the remaining arguments -/// .pick_or_route((), NoNameProvided::default().to_render()) -/// .unpack(); -/// -/// match picked { -/// Ok(value) => ParsedPickInput::new(value).to_render(), -/// Err(e) => e, -/// } +/// fn handle_greet(args: EntryGreet) -> Next { +/// let name: ResultName = args +/// .inner +/// .first() +/// .cloned() +/// .unwrap_or_else(|| "World".to_string()) +/// .into(); +/// name /// } /// /// #[renderer] -/// fn render_parsed_pick_input(prev: ParsedPickInput) { -/// let (age, name) = prev.inner; -/// r_println!("Picked: name = {}, age = {}", name, age); +/// fn render_name(name: ResultName) { +/// r_println!("Hello, {}!", *name); +/// } +/// +/// gen_program!(); +/// ``` +pub mod example_hook {} +/// Example Panic Unwind +/// +/// > This example introduces how to catch Panic in the Mingling program loop +/// +/// Run: +/// ```bash +/// cargo run --manifest-path examples/example-panic-unwind/Cargo.toml --quiet -- panic +/// cargo run --manifest-path examples/example-panic-unwind/Cargo.toml --quiet -- panic OhMyGod +/// ``` +/// +/// Output: +/// ```plaintext +/// Program not panic +/// Program panic: OhMyGod +/// OhMyGod +/// ``` +/// +/// Cargo.toml +/// ```ignore +/// [package] +/// name = "example-panic-unwind" +/// version = "0.1.0" +/// edition = "2024" +/// +/// [dependencies.mingling] +/// path = "../../mingling" +/// features = ["parser"] +/// +/// # Enable panic unwinding in release builds +/// [profile.release] +/// panic = "unwind" +/// +/// # Enable panic unwinding in dev builds +/// [profile.dev] +/// panic = "unwind" +/// ``` +/// +/// main.rs +/// ```ignore +/// use mingling::{hook::ProgramHook, prelude::*}; +/// +/// dispatcher!("panic", CMDPanic => EntryPanic); +/// pack!(NotPanic = ()); +/// +/// fn main() { +/// let mut program = ThisProgram::new(); +/// program.with_dispatcher(CMDPanic); +/// +/// // --------- IMPORTANT --------- +/// // Enable silence_panic to suppress automatic Panic output +/// program.stdout_setting.silence_panic = true; +/// +/// // Define a hook to output &ProgramPanic when a Panic occurs +/// program +/// .with_hook(ProgramHook::empty().on_exec_panic(|info| println!("Program panic: {}", info))); +/// // --------- IMPORTANT --------- +/// +/// program.exec(); +/// } +/// +/// #[chain] +/// fn handle_panic(prev: EntryPanic) -> Next { +/// let panic_info = prev.pick::<Option<String>>(()).unpack(); +/// match panic_info { +/// Some(s) => { +/// // Panic happens here, will be caught +/// panic!("{}", s) +/// } +/// None => NotPanic::default(), +/// } /// } /// /// #[renderer] -/// fn render_no_name_input(_prev: NoNameProvided) { -/// r_println!("No name provided."); +/// fn render(_: NotPanic) { +/// r_println!("Program not panic"); /// } /// /// gen_program!(); /// ``` -pub mod example_picker {} - +pub mod example_panic_unwind {} +/// Example REPL Basic +/// +/// > This example demonstrates how to develop a REPL program using the `repl` feature +/// +/// Run: +/// ```bash +/// cargo run --manifest-path examples/example-repl-basic/Cargo.toml --quiet +/// ``` /// /// Cargo.toml /// ```ignore /// [package] -/// name = "example-repl" -/// version = "0.0.1" +/// name = "example-repl-basic" +/// version = "0.1.0" /// edition = "2024" /// +/// [dependencies.mingling] +/// path = "../../mingling" +/// features = ["repl", "parser"] +/// /// [dependencies] -/// mingling = { path = "../../mingling", features = ["repl", "parser"] } /// just_fmt = "0.1.2" /// ``` /// @@ -591,11 +1334,11 @@ pub mod example_picker {} /// /// // Resource to store the current directory /// #[derive(Clone)] -/// struct CurrentDir { +/// struct ResCurrentDir { /// dir: PathBuf, /// } /// -/// impl Default for CurrentDir { +/// impl Default for ResCurrentDir { /// fn default() -> Self { /// Self { /// dir: current_dir().unwrap(), @@ -606,20 +1349,26 @@ pub mod example_picker {} /// fn main() { /// let mut program = ThisProgram::new(); /// -/// // Add resource -/// program.with_resource(CurrentDir::default()); +/// // Resource +/// program.with_resource(ResCurrentDir::default()); /// -/// // Add dispatchers +/// // Dispatchers /// program.with_dispatcher(ChangeDirectoryCommand); /// program.with_dispatcher(ListCommand); /// program.with_dispatcher(ExitCommand); /// program.with_dispatcher(ClearCommand); /// -/// // Add setups +/// // Setups +/// // Enable basic std::io::stdin().read_line(&mut input) /// program.with_setup(BasicREPLReadlineSetup); +/// +/// // Enable basic output, using println! after Renderer finishes drawing /// program.with_setup(BasicREPLOutputSetup); +/// +/// // Enable basic Prompt display, with custom display logic /// program.with_setup(BasicREPLPromptSetup::func(|| { -/// let res = this::<ThisProgram>().res::<CurrentDir>().unwrap(); +/// // Get the ResCurrentDir resource from the program +/// let res = this::<ThisProgram>().res::<ResCurrentDir>().unwrap(); /// let dir_str: String = res.dir.to_string_lossy().into(); /// let prompt = format!( /// "{}> ", @@ -665,9 +1414,11 @@ pub mod example_picker {} /// /// // Execute directory change /// #[chain] -/// fn handle_cd(prev: StateChangeDirectory, current_dir: &mut CurrentDir) -> Next { +/// fn handle_cd(prev: StateChangeDirectory, current_dir: &mut ResCurrentDir) -> Next { +/// use just_fmt::fmt_path::fmt_path; +/// /// let join = prev.inner; -/// let new_dir = just_fmt::fmt_path::fmt_path(current_dir.dir.join(join)).unwrap_or_default(); +/// let new_dir = fmt_path(current_dir.dir.join(join)).unwrap_or_default(); /// /// // If the path is not found, route to error handling /// if !new_dir.exists() { @@ -680,7 +1431,7 @@ pub mod example_picker {} /// /// // Get directory contents via the CurrentDir resource /// #[chain] -/// fn handle_ls(_prev: ListEntry, current_dir: &CurrentDir) -> Next { +/// fn handle_ls(_prev: ListEntry, current_dir: &ResCurrentDir) -> Next { /// let dir = ¤t_dir.dir; /// let entries: Vec<String> = std::fs::read_dir(dir) /// .into_iter() @@ -738,76 +1489,269 @@ pub mod example_picker {} /// /// gen_program!(); /// ``` -pub mod example_repl {} -/// `Mingling` Example - Global Resource Injection +pub mod example_repl_basic {} +/// Example Resource Injection /// -/// This example demonstrates how to use global resource injection in `#[chain]` functions. -/// You can inject both immutable (`&T`) and mutable (`&mut T`) references to global resources. +/// > This example demonstrates how to read and write the program's global state using Mingling's resource system /// -/// # How to Run +/// Run: /// ```bash -/// cargo run --manifest-path ./examples/example-resources/Cargo.toml -- setup +/// cargo run --manifest-path examples/example-resources/Cargo.toml --quiet current +/// cargo run --manifest-path examples/example-resources/Cargo.toml --quiet modify-current src +/// ``` +/// +/// Output: +/// ```plaintext +/// Current directory: /home/alice/mingling +/// Current directory: /home/alice/mingling/src /// ``` /// /// Cargo.toml /// ```ignore /// [package] /// name = "example-resources" -/// version = "0.0.1" +/// version = "0.1.0" /// edition = "2024" /// -/// [dependencies] -/// mingling = { path = "../../mingling", features = ["parser"] } +/// [dependencies.mingling] +/// path = "../../mingling" +/// features = ["parser"] /// ``` /// /// main.rs /// ```ignore +/// use std::path::PathBuf; +/// /// use mingling::prelude::*; -/// use std::{env::current_dir, path::PathBuf}; /// -/// // Define a resource for storing global state +/// // Create resource +/// // ______________ Resource needs to +/// // / / implement the following two traits +/// // vvvvvvv vvvvv /// #[derive(Default, Clone)] -/// pub struct MyResource { +/// struct ResCurrentDir { /// current_dir: PathBuf, /// } /// /// fn main() { /// let mut program = ThisProgram::new(); /// -/// // Add the resource to the program -/// program.with_resource(MyResource::default()); +/// // --------- IMPORTANT --------- +/// // Use `with_resource` to inject a singleton into the program +/// program.with_resource(ResCurrentDir { +/// current_dir: std::env::current_dir().unwrap(), +/// }); +/// // --------- IMPORTANT --------- /// -/// program.with_dispatcher(SetupCommand); +/// program.with_dispatchers((CMDCurrent, CMDModifyCurrent)); /// program.exec_and_exit(); /// } /// -/// dispatcher!("setup", SetupCommand => SetupEntry); -/// pack!(StateRead = ()); -/// pack!(ResultCurrentDir = PathBuf); +/// dispatcher!("current", CMDCurrent => EntryCurrent); +/// dispatcher!("modify-current", CMDModifyCurrent => EntryModifyCurrent); +/// +/// // Define chain for modifying current directory _________________ Injected muttable resource +/// // / +/// #[chain] // vvvvvvvvvvvvvvvvvv +/// fn render_modify_current(args: EntryModifyCurrent, current_dir: &mut ResCurrentDir) -> Next { +/// current_dir.current_dir = current_dir +/// .current_dir +/// .join(args.pick::<String>(()).unpack()); +/// EntryCurrent::default() +/// } /// -/// #[chain] -/// fn setup( -/// _prev: SetupEntry, -/// resource: &mut MyResource, // Import the resource into `setup` -/// ) -> Next { -/// // Set the global resource -/// resource.current_dir = current_dir().unwrap(); +/// // Define renderer for output current path _____________ Injected resource +/// // / +/// #[renderer] // vvvvvvvvvvvvvv +/// fn render_current(_: EntryCurrent, current_dir: &ResCurrentDir) { +/// r_println!("Current directory: {}", current_dir.current_dir.display()); +/// } +/// +/// gen_program!(); +/// ``` +pub mod example_resources {} +/// Example Setup +/// +/// > This example demonstrates how to build a custom Setup for modular management of project components +/// +/// Cargo.toml +/// ```ignore +/// [package] +/// name = "example-setup" +/// version = "0.1.0" +/// edition = "2024" +/// +/// [dependencies] +/// mingling = { path = "../../mingling" } +/// ``` +/// +/// main.rs +/// ```ignore +/// use mingling::{Program, macros::program_setup, prelude::*}; +/// +/// fn main() { +/// let mut program = ThisProgram::new(); +/// +/// // --------- IMPORTANT --------- +/// // Introduce `CustomSetup` generated by `custom_setup` +/// program.with_setup(CustomSetup); +/// // --------- IMPORTANT --------- +/// +/// program.exec_and_exit(); +/// } +/// +/// // --------- IMPORTANT --------- +/// // Define `CustomSetup` (inferred from `custom_setup`) +/// // Package part of the program construction logic into this type for modular management +/// #[program_setup] +/// fn custom_setup(program: &mut Program<ThisProgram>) { +/// program.with_dispatchers((CMD1, CMD2, CMD3, CMD4, CMD5)); +/// } +/// // --------- IMPORTANT --------- +/// +/// dispatcher!("1", CMD1 => Entry1); +/// dispatcher!("2", CMD2 => Entry2); +/// dispatcher!("3", CMD3 => Entry3); +/// dispatcher!("4", CMD4 => Entry4); +/// dispatcher!("5", CMD5 => Entry5); /// -/// StateRead::default() +/// gen_program!(); +/// ``` +pub mod example_setup {} +/// Example Unit Test +/// +/// > This example shows how to write unit tests for Chain and Renderer in Mingling +/// +/// ```bash +/// cargo test --manifest-path examples/example-unit-test/Cargo.toml +/// ``` +/// +/// Cargo.toml +/// ```ignore +/// [package] +/// name = "example-unit-test" +/// version = "0.1.0" +/// edition = "2024" +/// +/// [dependencies] +/// mingling = { path = "../../mingling" } +/// ``` +/// +/// main.rs +/// ```ignore +/// use mingling::prelude::*; +/// +/// #[cfg(test)] +/// mod tests { +/// use super::*; +/// use mingling::{assert_member_id, assert_render_result}; +/// +/// // --------- IMPORTANT --------- +/// #[test] +/// fn test_handle_hello() { +/// let hello_without_args = handle_hello(entry!()).into(); +/// assert_render_result!(hello_without_args); +/// assert_member_id!(hello_without_args, ThisProgram::ErrorNoNameProvided); +/// +/// let hello_with_registered_name = handle_hello(entry!("Alice")).into(); +/// assert_render_result!(hello_with_registered_name); +/// assert_member_id!( +/// hello_with_registered_name, +/// ThisProgram::ErrorNameNotAvailable +/// ); +/// +/// let hello_with_long_name = handle_hello(entry!("It's a VeryLongName")).into(); +/// assert_render_result!(hello_with_long_name); +/// assert_member_id!(hello_with_long_name, ThisProgram::ErrorNameTooLong); +/// +/// let hello_with_valid_name = handle_hello(entry!("Peter")).into(); +/// assert_render_result!(hello_with_valid_name); +/// } +/// +/// #[test] +/// fn test_render_result_name() { +/// let r = render_result_name(ResultName::new("Peter".into())); +/// assert_eq!(r, "Hello, Peter!\n") +/// } +/// +/// #[test] +/// fn test_render_error_no_name_provided() { +/// let r = render_error_no_name_provided(ErrorNoNameProvided::default()); +/// assert_eq!(r, "No name provided\n") +/// } +/// +/// #[test] +/// fn test_render_error_name_not_available() { +/// let r = render_error_name_not_available(ErrorNameNotAvailable::default()); +/// assert_eq!(r, "Name not available\n") +/// } +/// +/// #[test] +/// fn test_render_error_name_too_long() { +/// let r = render_error_name_too_long(ErrorNameTooLong::new(17)); +/// assert_eq!(r, "Name too long: 17 > 10\n") +/// } +/// // --------- IMPORTANT --------- /// } /// +/// dispatcher!("hello", CMDHello => EntryHello); +/// +/// pack!(ErrorNoNameProvided = ()); +/// pack!(ErrorNameTooLong = u16); +/// pack!(ErrorNameNotAvailable = ()); +/// +/// pack!(ResultName = String); +/// +/// static VEC_REGISTERED_NAMES: &[&str] = &["Alice", "Bob", "Charlie", "David", "Eve"]; +/// /// #[chain] -/// fn read(_prev: StateRead, resource: &MyResource) -> Next { -/// // Read the global resource -/// let current_dir = resource.current_dir.clone(); -/// ResultCurrentDir::new(current_dir).to_render() +/// fn handle_hello(args: EntryHello) -> Next { +/// let Some(name) = args.inner.first().cloned() else { +/// return ErrorNoNameProvided::default().to_render(); +/// }; +/// +/// if name.len() > 10 { +/// return ErrorNameTooLong::new(name.len() as u16).to_render(); +/// } +/// +/// if VEC_REGISTERED_NAMES.contains(&name.as_str()) { +/// return ErrorNameNotAvailable::default().to_render(); +/// } +/// +/// ResultName::new(name).to_render() +/// } +/// +/// #[renderer] +/// fn render_result_name(name: ResultName) -> String { +/// r_println!("Hello, {}!", *name); /// } /// /// #[renderer] -/// fn render_current_dir(dir: ResultCurrentDir) { -/// r_println!("Current dir: {}", dir.to_string_lossy()) +/// fn render_error_no_name_provided(_: ErrorNoNameProvided) -> String { +/// r_println!("No name provided"); +/// } +/// +/// #[renderer] +/// fn render_error_name_not_available(_: ErrorNameNotAvailable) -> String { +/// r_println!("Name not available"); +/// } +/// +/// #[renderer] +/// fn render_error_name_too_long(len: ErrorNameTooLong) -> String { +/// r_println!("Name too long: {} > 10", *len); +/// } +/// +/// #[renderer] +/// fn render_dispatcher_not_found(err: DispatcherNotFound) { +/// r_println!("Command not found: \"{}\"", err.inner.join(" ")); /// } /// /// gen_program!(); +/// +/// fn main() { +/// let mut program = ThisProgram::new(); +/// program.with_dispatcher(CMDHello); +/// program.exec_and_exit(); +/// } /// ``` -pub mod example_resources {} +pub mod example_unit_test {} diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs index 4c11b7c..583c89a 100644 --- a/mingling/src/lib.rs +++ b/mingling/src/lib.rs @@ -85,6 +85,8 @@ pub mod macros { 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; + /// Creates a packed entry value from a list of string literals + pub use mingling_macros::entry; /// 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 @@ -216,6 +218,8 @@ pub mod prelude { 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 `entry` macro for creating packed entry values from string literals. + pub use crate::macros::entry; /// 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. |
