diff options
| author | Weicao-CatilGrass <1992414357@qq.com> | 2026-05-23 23:41:04 +0800 |
|---|---|---|
| committer | Weicao-CatilGrass <1992414357@qq.com> | 2026-05-23 23:49:34 +0800 |
| commit | 0a2ef958c0dca21d19e4ffc38ba5a7c4078e182a (patch) | |
| tree | c82fc4242ed393b132ba514eb434d722e7d9c387 /examples | |
| parent | ccab1940c019dfbfb7dfcbbe4cb927258933755f (diff) | |
Rework examples and add entry macro for testing
Diffstat (limited to 'examples')
60 files changed, 2209 insertions, 768 deletions
diff --git a/examples/example-argument-parse/Cargo.lock b/examples/example-argument-parse/Cargo.lock new file mode 100644 index 0000000..b035436 --- /dev/null +++ b/examples/example-argument-parse/Cargo.lock @@ -0,0 +1,83 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "example-argument-parse" +version = "0.1.0" +dependencies = [ + "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", +] + +[[package]] +name = "mingling_macros" +version = "0.1.9" +dependencies = [ + "just_fmt", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "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-argument-parse/Cargo.toml b/examples/example-argument-parse/Cargo.toml new file mode 100644 index 0000000..3b06523 --- /dev/null +++ b/examples/example-argument-parse/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "example-argument-parse" +version = "0.1.0" +edition = "2024" + +[dependencies.mingling] +path = "../../mingling" + +# Enable `parser` features +features = ["parser"] diff --git a/examples/example-argument-parse/src/main.rs b/examples/example-argument-parse/src/main.rs new file mode 100644 index 0000000..f63fdad --- /dev/null +++ b/examples/example-argument-parse/src/main.rs @@ -0,0 +1,95 @@ +//! Example Argument Parse +//! +//! > This example demonstrates how to use the `parser` feature to parse user input +//! +//! 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 +//! ``` +//! +//! Output: +//! ```plaintext +//! file: README.md (32768) +//! dir: src/ (1048576) +//! file: README.md (1048576) +//! Error: name is not provided +//! ``` + +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(); +} diff --git a/examples/example-picker/Cargo.lock b/examples/example-async-support/Cargo.lock index eada902..1c4d5b1 100644 --- a/examples/example-picker/Cargo.lock +++ b/examples/example-async-support/Cargo.lock @@ -3,8 +3,8 @@ version = 4 [[package]] -name = "example-picker" -version = "0.0.1" +name = "example-async-support" +version = "0.1.0" dependencies = [ "mingling", "tokio", @@ -85,9 +85,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.52.1" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "pin-project-lite", "tokio-macros", diff --git a/examples/example-async-support/Cargo.toml b/examples/example-async-support/Cargo.toml new file mode 100644 index 0000000..3a66e52 --- /dev/null +++ b/examples/example-async-support/Cargo.toml @@ -0,0 +1,15 @@ +[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"] diff --git a/examples/example-async-support/src/main.rs b/examples/example-async-support/src/main.rs new file mode 100644 index 0000000..12b1b9c --- /dev/null +++ b/examples/example-async-support/src/main.rs @@ -0,0 +1,68 @@ +//! Example Async Runtime Support +//! +//! > 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. +//! ``` + +use mingling::{hook::ProgramHook, prelude::*}; + +#[tokio::main] +async fn main() { + let mut program = ThisProgram::new(); + + 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 --------- +} + +dispatcher!("download", CMDDownload => EntryDownload); + +pack!(ResultDownloaded = String); + +// --------- IMPORTANT --------- +#[chain] +// 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 +} + +#[renderer] +// 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) +} diff --git a/examples/example-async/Cargo.lock b/examples/example-async/Cargo.lock deleted file mode 100644 index b7dd453..0000000 --- a/examples/example-async/Cargo.lock +++ /dev/null @@ -1,250 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "bitflags" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys", -] - -[[package]] -name = "example-async" -version = "0.0.1" -dependencies = [ - "mingling", - "tokio", -] - -[[package]] -name = "just_fmt" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "mingling" -version = "0.1.9" -dependencies = [ - "mingling_core", - "mingling_macros", -] - -[[package]] -name = "mingling_core" -version = "0.1.9" -dependencies = [ - "just_fmt", -] - -[[package]] -name = "mingling_macros" -version = "0.1.9" -dependencies = [ - "just_fmt", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "mio" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" -dependencies = [ - "libc", - "wasi", - "windows-sys", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[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 = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" -dependencies = [ - "libc", - "windows-sys", -] - -[[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 = "tokio" -version = "1.52.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys", -] - -[[package]] -name = "tokio-macros" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] diff --git a/examples/example-async/Cargo.toml b/examples/example-async/Cargo.toml deleted file mode 100644 index 0089520..0000000 --- a/examples/example-async/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "example-async" -version = "0.0.1" -edition = "2024" - -[dependencies] -tokio = { version = "1", features = ["full"] } -mingling = { path = "../../mingling", features = ["async"] } diff --git a/examples/example-async/src/main.rs b/examples/example-async/src/main.rs deleted file mode 100644 index 7b0be38..0000000 --- a/examples/example-async/src/main.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! `Mingling` Example - Async -//! -//! 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 -//! -//! ## Enable Feature -//! Enable the `async` feature for mingling in `Cargo.toml` -//! ```toml -//! [dependencies] -//! mingling = { version = "...", features = ["async"] } -//! ``` -//! -//! # How to Run -//! ```bash -//! cargo run --manifest-path ./examples/example-async/Cargo.toml -- hello World -//! ``` - -use mingling::prelude::*; - -dispatcher!("hello", HelloCommand => HelloEntry); - -// Use Tokio async runtime -#[tokio::main] -async fn main() { - let mut program = ThisProgram::new(); - program.with_dispatcher(HelloCommand); - - // Run program - program.exec().await; -} - -pack!(Hello = String); - -// You can freely use async / non-async functions to declare your Chain - -#[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() -} - -// For renderers, you can still only use synchronous functions -#[renderer] -fn render_hello_who(prev: Hello) { - r_println!("Hello, {}!", *prev); -} - -gen_program!(); diff --git a/examples/example-basic/Cargo.lock b/examples/example-basic/Cargo.lock index de110d2..e8e6ba6 100644 --- a/examples/example-basic/Cargo.lock +++ b/examples/example-basic/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "example-basic" -version = "0.0.1" +version = "0.1.0" dependencies = [ "mingling", ] diff --git a/examples/example-basic/Cargo.toml b/examples/example-basic/Cargo.toml index 48c281a..c8504fc 100644 --- a/examples/example-basic/Cargo.toml +++ b/examples/example-basic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "example-basic" -version = "0.0.1" +version = "0.1.0" edition = "2024" [dependencies] diff --git a/examples/example-basic/src/main.rs b/examples/example-basic/src/main.rs index 33840fc..d741c3b 100644 --- a/examples/example-basic/src/main.rs +++ b/examples/example-basic/src/main.rs @@ -1,47 +1,70 @@ -//! `Mingling` Example - Basic +//! 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! //! ``` +// 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); +// Quickly wrap a type into a type recognizable by the current program +// ____________________ Wrapped type name +// / _______ Wrapped type inner value +// | / +// vvvvvvvvvv vvvvvv +pack!(ResultName = 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() +// 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!(); diff --git a/examples/example-completion/Cargo.lock b/examples/example-completion/Cargo.lock index d027a14..ffc92a5 100644 --- a/examples/example-completion/Cargo.lock +++ b/examples/example-completion/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "example-completion" -version = "0.0.1" +version = "0.1.0" dependencies = [ "mingling", ] diff --git a/examples/example-completion/Cargo.toml b/examples/example-completion/Cargo.toml index 6a76d1f..4e1881a 100644 --- a/examples/example-completion/Cargo.toml +++ b/examples/example-completion/Cargo.toml @@ -1,7 +1,25 @@ [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", +] diff --git a/examples/example-completion/build.rs b/examples/example-completion/build.rs new file mode 100644 index 0000000..e1ffba1 --- /dev/null +++ b/examples/example-completion/build.rs @@ -0,0 +1,14 @@ +fn main() { + build_scripts(); +} + +/// 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(); +} diff --git a/examples/example-completion/src/main.rs b/examples/example-completion/src/main.rs index 31528d1..7ca23b9 100644 --- a/examples/example-completion/src/main.rs +++ b/examples/example-completion/src/main.rs @@ -1,136 +1,129 @@ -//! `Mingling` Example - Completion +//! Example Completion //! -//! # How to Deploy -//! 1. Enable the `comp` feature -//! ```toml -//! [dependencies] -//! mingling = { version = "...", features = [ -//! "comp", // Enable this feature -//! "parser" -//! ] } -//! ``` +//! > This example demonstrates how to use **Mingling** to create fully dynamic command-line completions //! -//! 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" -//! ] } -//! ``` +//! ## About Completion Scripts +//! +//! 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` //! -//! 3. Write `build.rs` to generate completion scripts at compile time -//! ```ignore -//! use mingling::build::{build_comp_scripts, build_comp_scripts_with_bin_name}; +//! ```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 - -use mingling::prelude::*; -use mingling::{ - macros::{suggest, suggest_enum}, - parser::{PickableEnum, Picker}, - EnumTag, Groupped, ShellContext, Suggest, -}; - -// Define dispatcher `FruitCommand`, directing subcommand "fruit" to `FruitEntry` -dispatcher!("fruit", FruitCommand => FruitEntry); +//! 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! +//! ``` -#[completion(FruitEntry)] -fn comp_fruit_command(ctx: &ShellContext) -> Suggest { - if ctx.filling_argument_first("--name") { - return suggest!(); - } - if ctx.filling_argument_first("--type") { - return suggest_enum!(FruitType); - } - if ctx.typing_argument() { - return suggest! { - "--name": "Fruit name", - "--type": "Fruit type" - } - .strip_typed_argument(ctx); - } - return suggest!(); -} +use mingling::{macros::suggest, prelude::*, ShellContext, Suggest}; fn main() { let mut program = ThisProgram::new(); - program.with_dispatcher(CompletionDispatcher); - program.with_dispatcher(FruitCommand); - program.exec(); -} -#[derive(Groupped)] -struct FruitInfo { - name: String, - fruit_type: FruitType, -} + program.with_dispatcher(CMDGreet); -#[derive(Default, Debug, EnumTag)] -enum FruitType { - #[enum_desc("It's Apple")] - #[enum_rename("apple")] - FruitApple, + // --------- 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 --------- - #[enum_desc("It's Banana")] - #[enum_rename("banana")] - FruitBanana, + // TIP: Note that the completion script reads stdout, + // so make sure no output is produced before the CompletionDispatcher is dispatched. + program.exec_and_exit(); +} - #[enum_desc("It's Cherry")] - #[enum_rename("cherry")] - FruitCherry, +// --------- 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" + }; + } - #[enum_desc("It's Date")] - #[enum_rename("date")] - FruitDate, + // When the user is typing `--repeat` + if ctx.filling_argument(["-r", "--repeat"]) { + return suggest! {}; // Don't suggest anything + } - #[enum_desc("It's Elderberry")] - #[enum_rename("elderberry")] - FruitElderberry, + // When the user is typing `-` + if ctx.typing_argument() { + return suggest! { + "-r": "Number of repetitions", + "--repeat": "Number of repetitions", + } + // Remove arguments that have already been typed by the user + .strip_typed_argument(ctx); + } - #[default] - #[enum_rename("unknown")] - Unknown, + // 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 --------- -impl PickableEnum for FruitType {} +dispatcher!("greet", CMDGreet => EntryGreet); +pack!(ResultName = (u8, String)); #[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_greet(args: EntryGreet) -> Next { + let result: ResultName = args + .pick(["-r", "--repeat"]) + .pick_or((), "World") + .unpack() + .into(); + result } #[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); - } - (false, FruitType::Unknown) => { - r_println!("Fruit name: {}, Type is unknown", prev.name); - } - (false, fruit_type) => { - r_println!("Fruit name: {}, Type: {:?}", prev.name, fruit_type); - } +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(", ")); } gen_program!(); diff --git a/examples/example-custom-pickable/Cargo.lock b/examples/example-custom-pickable/Cargo.lock new file mode 100644 index 0000000..8ef90ec --- /dev/null +++ b/examples/example-custom-pickable/Cargo.lock @@ -0,0 +1,83 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "example-custom-pickable" +version = "0.1.0" +dependencies = [ + "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", +] + +[[package]] +name = "mingling_macros" +version = "0.1.9" +dependencies = [ + "just_fmt", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "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-custom-pickable/Cargo.toml b/examples/example-custom-pickable/Cargo.toml new file mode 100644 index 0000000..ca97c4a --- /dev/null +++ b/examples/example-custom-pickable/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "example-custom-pickable" +version = "0.1.0" +edition = "2024" + +[dependencies.mingling] +path = "../../mingling" + +features = [ + "parser", +] diff --git a/examples/example-custom-pickable/src/main.rs b/examples/example-custom-pickable/src/main.rs new file mode 100644 index 0000000..466ae43 --- /dev/null +++ b/examples/example-custom-pickable/src/main.rs @@ -0,0 +1,125 @@ +//! Example Custom Pickable +//! +//! > This example demonstrates how to use the Pickable trait to add parsing for your types +//! +//! 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 +//! ``` +//! +//! Output: +//! ```plaintext +//! Connected to "127.0.0.1:5012" +//! Failed to parse address +//! ``` + +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 --------- + +dispatcher!("connect", CMDConnect => EntryConnect); +pack!(ErrorParseAddressFailed = ()); + +#[chain] +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_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()); + } + + 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()); + } + + 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 }) + } +} + +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 + ) + } +} diff --git a/examples/example-dispatch-tree/Cargo.lock b/examples/example-dispatch-tree/Cargo.lock index 4085ce5..4b6c7eb 100644 --- a/examples/example-dispatch-tree/Cargo.lock +++ b/examples/example-dispatch-tree/Cargo.lock @@ -16,15 +16,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" [[package]] -name = "just_template" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3edb658c34b10b69c4b3b58f7ba989cd09c82c0621dee1eef51843c2327225" -dependencies = [ - "just_fmt", -] - -[[package]] name = "mingling" version = "0.1.9" dependencies = [ @@ -37,7 +28,6 @@ name = "mingling_core" version = "0.1.9" dependencies = [ "just_fmt", - "just_template", ] [[package]] diff --git a/examples/example-dispatch-tree/Cargo.toml b/examples/example-dispatch-tree/Cargo.toml index f1c6785..0c31ecb 100644 --- a/examples/example-dispatch-tree/Cargo.toml +++ b/examples/example-dispatch-tree/Cargo.toml @@ -3,5 +3,9 @@ name = "example-dispatch-tree" version = "0.1.0" edition = "2024" -[dependencies] -mingling = { path = "../../mingling", features = ["dispatch_tree", "comp"] } +[dependencies.mingling] +path = "../../mingling" + +features = [ + "dispatch_tree", +] diff --git a/examples/example-dispatch-tree/src/main.rs b/examples/example-dispatch-tree/src/main.rs index d8be32a..08714d1 100644 --- a/examples/example-dispatch-tree/src/main.rs +++ b/examples/example-dispatch-tree/src/main.rs @@ -1,47 +1,59 @@ -//! `Mingling` Example - Dispatch Tree +//! 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 -//! ] } -//! ``` +//! > This example will introduce how to use `dispatch_tree` +//! > to optimize your command line lookup efficiency +//! +//! 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. //! -//! 2. Using `cargo expand`: +//! 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 //! ``` - -#![allow(unused_mut)] +//! +//! Output: +//! ```plaintext +//! It's works! +//! ``` +//! 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!(); diff --git a/examples/example-enum-tag/Cargo.lock b/examples/example-enum-tag/Cargo.lock new file mode 100644 index 0000000..9839796 --- /dev/null +++ b/examples/example-enum-tag/Cargo.lock @@ -0,0 +1,93 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "example-enum-tag" +version = "0.1.0" +dependencies = [ + "mingling", +] + +[[package]] +name = "just_fmt" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" + +[[package]] +name = "just_template" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3edb658c34b10b69c4b3b58f7ba989cd09c82c0621dee1eef51843c2327225" +dependencies = [ + "just_fmt", +] + +[[package]] +name = "mingling" +version = "0.1.9" +dependencies = [ + "mingling_core", + "mingling_macros", + "size", +] + +[[package]] +name = "mingling_core" +version = "0.1.9" +dependencies = [ + "just_fmt", + "just_template", +] + +[[package]] +name = "mingling_macros" +version = "0.1.9" +dependencies = [ + "just_fmt", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "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-enum-tag/Cargo.toml b/examples/example-enum-tag/Cargo.toml new file mode 100644 index 0000000..7a8e5d6 --- /dev/null +++ b/examples/example-enum-tag/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "example-enum-tag" +version = "0.1.0" +edition = "2024" + +[dependencies.mingling] +path = "../../mingling" + +features = [ + "comp", + "parser" +] diff --git a/examples/example-enum-tag/src/main.rs b/examples/example-enum-tag/src/main.rs new file mode 100644 index 0000000..05419f7 --- /dev/null +++ b/examples/example-enum-tag/src/main.rs @@ -0,0 +1,102 @@ +//! Example Enum Tag +//! +//! > This example demonstrates how to use the `EnumTag` derive macro to tag enum variants with metadata, +//! > which can be used for autocompletion and parsing +//! +//! Run: +//! ```bash +//! 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) +//! ``` + +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(CompletionDispatcher); + program.with_dispatcher(CMDLanguageSelection); + program.exec_and_exit(); +} diff --git a/examples/example-error-handling/Cargo.lock b/examples/example-error-handling/Cargo.lock new file mode 100644 index 0000000..d2deef4 --- /dev/null +++ b/examples/example-error-handling/Cargo.lock @@ -0,0 +1,76 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "example-error-handling" +version = "0.1.0" +dependencies = [ + "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", +] + +[[package]] +name = "mingling_core" +version = "0.1.9" +dependencies = [ + "just_fmt", +] + +[[package]] +name = "mingling_macros" +version = "0.1.9" +dependencies = [ + "just_fmt", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "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-error-handling/Cargo.toml b/examples/example-error-handling/Cargo.toml new file mode 100644 index 0000000..c6e7ea3 --- /dev/null +++ b/examples/example-error-handling/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "example-error-handling" +version = "0.1.0" +edition = "2024" + +[dependencies] +mingling = { path = "../../mingling" } diff --git a/examples/example-error-handling/src/main.rs b/examples/example-error-handling/src/main.rs new file mode 100644 index 0000000..d8db852 --- /dev/null +++ b/examples/example-error-handling/src/main.rs @@ -0,0 +1,97 @@ +//! Example Error Handling +//! +//! > 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 +//! ``` + +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_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_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(); +} diff --git a/examples/example-exit-code/src/main.rs b/examples/example-exit-code/src/main.rs deleted file mode 100644 index c9b3c92..0000000 --- a/examples/example-exit-code/src/main.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! `Mingling` Example - Exit Code -//! -//! 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. -//! -//! # How to Run -//! ```bash -//! cargo run --manifest-path ./examples/example-exit-code/Cargo.toml -- error -//! ``` - -use mingling::prelude::*; -use mingling::{res::ExitCode, setup::ExitCodeSetup}; - -fn main() { - let mut program = ThisProgram::new(); - program.with_dispatcher(ErrorCommand); - program.with_setup(ExitCodeSetup::<ThisProgram>::default()); - program.exec_and_exit(); -} - -dispatcher!("error", ErrorCommand => ErrorEntry); -pack!(ResultError = ()); - -#[chain] -fn handle_error_entry(_prev: ErrorEntry, ec: &mut ExitCode) -> Next { - ec.exit_code = 1; - return ResultError::default(); -} - -#[renderer] -fn render_error(_prev: ResultError, ec: &ExitCode) { - r_println!("Exit with exit code: {}", ec.exit_code); -} - -gen_program!(); diff --git a/examples/example-exit-code/Cargo.lock b/examples/example-exitcode/Cargo.lock index 2ba3ffe..19f60a6 100644 --- a/examples/example-exit-code/Cargo.lock +++ b/examples/example-exitcode/Cargo.lock @@ -3,7 +3,7 @@ version = 4 [[package]] -name = "example-exit-code" +name = "example-exitcode" version = "0.1.0" dependencies = [ "mingling", diff --git a/examples/example-exit-code/Cargo.toml b/examples/example-exitcode/Cargo.toml index a6c9c7c..58c800b 100644 --- a/examples/example-exit-code/Cargo.toml +++ b/examples/example-exitcode/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "example-exit-code" +name = "example-exitcode" version = "0.1.0" edition = "2024" diff --git a/examples/example-exitcode/src/main.rs b/examples/example-exitcode/src/main.rs new file mode 100644 index 0000000..178fa78 --- /dev/null +++ b/examples/example-exitcode/src/main.rs @@ -0,0 +1,62 @@ +//! Example Error Handling +//! +//! > This example demonstrates how to handle errors in Mingling, including custom error types and error rendering. +//! +//! 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 +//! ``` +//! +//! Output: +//! ```plaintext +//! Hello, Alice +//! No name provided (with exit code 1) +//! ``` + +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!(); diff --git a/examples/example-general-renderer/Cargo.lock b/examples/example-general-renderer/Cargo.lock index f0f0eea..e8de59f 100644 --- a/examples/example-general-renderer/Cargo.lock +++ b/examples/example-general-renderer/Cargo.lock @@ -3,36 +3,14 @@ version = 4 [[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] name = "example-general-renderer" -version = "0.0.1" +version = "0.1.0" dependencies = [ "mingling", "serde", ] [[package]] -name = "hashbrown" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -67,7 +45,6 @@ dependencies = [ "just_fmt", "serde", "serde_json", - "serde_yaml", ] [[package]] @@ -99,12 +76,6 @@ dependencies = [ ] [[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -136,9 +107,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -148,19 +119,6 @@ dependencies = [ ] [[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - -[[package]] name = "size" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -184,12 +142,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - -[[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/examples/example-general-renderer/Cargo.toml b/examples/example-general-renderer/Cargo.toml index 4664e88..7fd4fd5 100644 --- a/examples/example-general-renderer/Cargo.toml +++ b/examples/example-general-renderer/Cargo.toml @@ -1,13 +1,14 @@ [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", +] diff --git a/examples/example-general-renderer/src/main.rs b/examples/example-general-renderer/src/main.rs index 64d8d00..5f74815 100644 --- a/examples/example-general-renderer/src/main.rs +++ b/examples/example-general-renderer/src/main.rs @@ -1,30 +1,15 @@ -//! `Mingling` Example - General Renderer +//! Example General Renderer //! -//! ## Step1 - Enable Feature -//! Enable the `general_renderer` feature for mingling in `Cargo.toml` -//! ```toml -//! [dependencies] -//! mingling = { version = "...", features = ["general_renderer", "parser"] } -//! ``` -//! -//! ## Step2 - Add Dependencies -//! Add `serde` dependency to `Cargo.toml` for serialization support -//! ```toml -//! [dependencies] -//! serde = { version = "1", features = ["derive"] } -//! ``` +//! > This example demonstrates how to use the `general_renderer` feature to render data into structures such as json / yaml //! -//! ## Step3 - Write Code -//! Write the following content into `main.rs` -//! -//! ## Step4 - Build and Run +//! 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} @@ -33,7 +18,7 @@ //! ``` use mingling::prelude::*; -use mingling::{parser::Picker, setup::GeneralRendererSetup, Groupped}; +use mingling::{Groupped, parser::Picker, setup::GeneralRendererSetup}; use serde::Serialize; dispatcher!("render", RenderCommand => RenderCommandEntry); @@ -46,7 +31,13 @@ fn main() { 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")] @@ -54,6 +45,12 @@ struct Info { #[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 { diff --git a/examples/example-help/Cargo.lock b/examples/example-help/Cargo.lock new file mode 100644 index 0000000..5e16d19 --- /dev/null +++ b/examples/example-help/Cargo.lock @@ -0,0 +1,76 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "example-help" +version = "0.1.0" +dependencies = [ + "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", +] + +[[package]] +name = "mingling_core" +version = "0.1.9" +dependencies = [ + "just_fmt", +] + +[[package]] +name = "mingling_macros" +version = "0.1.9" +dependencies = [ + "just_fmt", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "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-help/Cargo.toml b/examples/example-help/Cargo.toml new file mode 100644 index 0000000..01e6801 --- /dev/null +++ b/examples/example-help/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "example-help" +version = "0.1.0" +edition = "2024" + +[dependencies] +mingling = { path = "../../mingling" } diff --git a/examples/example-help/src/main.rs b/examples/example-help/src/main.rs new file mode 100644 index 0000000..9567c49 --- /dev/null +++ b/examples/example-help/src/main.rs @@ -0,0 +1,41 @@ +//! Example Help +//! +//! > 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 +//! ``` +//! +//! Output: +//! ```plain +//! Usage: greet <NAME> +//! ``` + +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!(); diff --git a/examples/example-hook/Cargo.lock b/examples/example-hook/Cargo.lock new file mode 100644 index 0000000..b6717bb --- /dev/null +++ b/examples/example-hook/Cargo.lock @@ -0,0 +1,76 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "example-hook" +version = "0.1.0" +dependencies = [ + "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", +] + +[[package]] +name = "mingling_core" +version = "0.1.9" +dependencies = [ + "just_fmt", +] + +[[package]] +name = "mingling_macros" +version = "0.1.9" +dependencies = [ + "just_fmt", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "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-hook/Cargo.toml b/examples/example-hook/Cargo.toml new file mode 100644 index 0000000..453fae6 --- /dev/null +++ b/examples/example-hook/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "example-hook" +version = "0.1.0" +edition = "2024" + +[dependencies] +mingling = { path = "../../mingling" } diff --git a/examples/example-hook/src/main.rs b/examples/example-hook/src/main.rs new file mode 100644 index 0000000..373c76d --- /dev/null +++ b/examples/example-hook/src/main.rs @@ -0,0 +1,71 @@ +//! 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! +//! ``` + +use mingling::{hook::ProgramHook, prelude::*}; + +dispatcher!("greet", CMDGreet => EntryGreet); + +fn main() { + let mut program = ThisProgram::new(); + + // --------- 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!(ResultName = String); + +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + let name: ResultName = args + .inner + .first() + .cloned() + .unwrap_or_else(|| "World".to_string()) + .into(); + name +} + +#[renderer] +fn render_name(name: ResultName) { + r_println!("Hello, {}!", *name); +} + +gen_program!(); diff --git a/examples/example-panic-unwind/Cargo.lock b/examples/example-panic-unwind/Cargo.lock new file mode 100644 index 0000000..d6af75d --- /dev/null +++ b/examples/example-panic-unwind/Cargo.lock @@ -0,0 +1,83 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "example-panic-unwind" +version = "0.1.0" +dependencies = [ + "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", +] + +[[package]] +name = "mingling_macros" +version = "0.1.9" +dependencies = [ + "just_fmt", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "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-panic-unwind/Cargo.toml b/examples/example-panic-unwind/Cargo.toml new file mode 100644 index 0000000..5d1c242 --- /dev/null +++ b/examples/example-panic-unwind/Cargo.toml @@ -0,0 +1,16 @@ +[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" diff --git a/examples/example-panic-unwind/src/main.rs b/examples/example-panic-unwind/src/main.rs new file mode 100644 index 0000000..18cf4d6 --- /dev/null +++ b/examples/example-panic-unwind/src/main.rs @@ -0,0 +1,56 @@ +//! 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 +//! ``` + +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(_: NotPanic) { + r_println!("Program not panic"); +} + +gen_program!(); diff --git a/examples/example-picker/Cargo.toml b/examples/example-picker/Cargo.toml deleted file mode 100644 index d8e9c86..0000000 --- a/examples/example-picker/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "example-picker" -version = "0.0.1" -edition = "2024" - -[dependencies] -mingling = { path = "../../mingling", features = ["parser"] } -tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } diff --git a/examples/example-picker/src/main.rs b/examples/example-picker/src/main.rs deleted file mode 100644 index 651edb3..0000000 --- a/examples/example-picker/src/main.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! `Mingling` Example - Picker -//! -//! ## Step1 - Enable Feature -//! Enable the `parser` feature for mingling in `Cargo.toml` -//! ```toml -//! [dependencies] -//! mingling = { version = "...", features = ["parser"] } -//! ``` -//! -//! ## Step2 - Write Code -//! Write the following content into `main.rs` -//! -//! ## 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 -//! ``` - -use mingling::prelude::*; - -dispatcher!("pick", PickCommand => PickEntry); - -fn main() { - let mut program = ThisProgram::new(); - program.with_dispatcher(PickCommand); - program.exec(); -} - -pack!(NoNameProvided = ()); -pack!(ParsedPickInput = (i32, 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, - } -} - -#[renderer] -fn render_parsed_pick_input(prev: ParsedPickInput) { - let (age, name) = prev.inner; - r_println!("Picked: name = {}, age = {}", name, age); -} - -#[renderer] -fn render_no_name_input(_prev: NoNameProvided) { - r_println!("No name provided."); -} - -gen_program!(); diff --git a/examples/example-repl/Cargo.lock b/examples/example-repl-basic/Cargo.lock index adde389..854e482 100644 --- a/examples/example-repl/Cargo.lock +++ b/examples/example-repl-basic/Cargo.lock @@ -3,8 +3,8 @@ version = 4 [[package]] -name = "example-repl" -version = "0.0.1" +name = "example-repl-basic" +version = "0.1.0" dependencies = [ "just_fmt", "mingling", diff --git a/examples/example-repl-basic/Cargo.toml b/examples/example-repl-basic/Cargo.toml new file mode 100644 index 0000000..c358a73 --- /dev/null +++ b/examples/example-repl-basic/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "example-repl-basic" +version = "0.1.0" +edition = "2024" + +[dependencies.mingling] +path = "../../mingling" +features = ["repl", "parser"] + +[dependencies] +just_fmt = "0.1.2" diff --git a/examples/example-repl/src/main.rs b/examples/example-repl-basic/src/main.rs index 2d8d9b0..f02c2f8 100644 --- a/examples/example-repl/src/main.rs +++ b/examples/example-repl-basic/src/main.rs @@ -1,3 +1,12 @@ +//! 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 +//! ``` + use mingling::{ REPL, hook::ProgramHook, @@ -9,11 +18,11 @@ use std::{env::current_dir, path::PathBuf}; // 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(), @@ -24,20 +33,26 @@ impl Default for CurrentDir { 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!( "{}> ", @@ -83,9 +98,11 @@ fn parse_cd_args(prev: ChangeDirectoryEntry) -> Next { // 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() { @@ -98,7 +115,7 @@ fn handle_cd(prev: StateChangeDirectory, current_dir: &mut CurrentDir) -> Next { // 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() diff --git a/examples/example-repl/Cargo.toml b/examples/example-repl/Cargo.toml deleted file mode 100644 index 34b85e3..0000000 --- a/examples/example-repl/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[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-resources/Cargo.lock b/examples/example-resources/Cargo.lock index 63467d0..bab1a19 100644 --- a/examples/example-resources/Cargo.lock +++ b/examples/example-resources/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "example-resources" -version = "0.0.1" +version = "0.1.0" dependencies = [ "mingling", ] diff --git a/examples/example-resources/Cargo.toml b/examples/example-resources/Cargo.toml index ca783f3..2655654 100644 --- a/examples/example-resources/Cargo.toml +++ b/examples/example-resources/Cargo.toml @@ -1,7 +1,8 @@ [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"] diff --git a/examples/example-resources/src/main.rs b/examples/example-resources/src/main.rs index 9d8dde6..ae2c8f2 100644 --- a/examples/example-resources/src/main.rs +++ b/examples/example-resources/src/main.rs @@ -1,57 +1,64 @@ -//! `Mingling` Example - Global Resource Injection +//! 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 +//! ``` + +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); - -#[chain] -fn setup( - _prev: SetupEntry, - resource: &mut MyResource, // Import the resource into `setup` -) -> Next { - // Set the global resource - resource.current_dir = current_dir().unwrap(); - - StateRead::default() -} +dispatcher!("current", CMDCurrent => EntryCurrent); +dispatcher!("modify-current", CMDModifyCurrent => EntryModifyCurrent); -#[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() +// 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() } -#[renderer] -fn render_current_dir(dir: ResultCurrentDir) { - r_println!("Current dir: {}", dir.to_string_lossy()) +// 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!(); diff --git a/examples/example-setup/Cargo.lock b/examples/example-setup/Cargo.lock new file mode 100644 index 0000000..6b8f0d4 --- /dev/null +++ b/examples/example-setup/Cargo.lock @@ -0,0 +1,76 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "example-setup" +version = "0.1.0" +dependencies = [ + "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", +] + +[[package]] +name = "mingling_core" +version = "0.1.9" +dependencies = [ + "just_fmt", +] + +[[package]] +name = "mingling_macros" +version = "0.1.9" +dependencies = [ + "just_fmt", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "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-setup/Cargo.toml b/examples/example-setup/Cargo.toml new file mode 100644 index 0000000..12364aa --- /dev/null +++ b/examples/example-setup/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "example-setup" +version = "0.1.0" +edition = "2024" + +[dependencies] +mingling = { path = "../../mingling" } diff --git a/examples/example-setup/src/main.rs b/examples/example-setup/src/main.rs new file mode 100644 index 0000000..c445276 --- /dev/null +++ b/examples/example-setup/src/main.rs @@ -0,0 +1,33 @@ +//! Example Setup +//! +//! > This example demonstrates how to build a custom Setup for modular management of project components + +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); + +gen_program!(); diff --git a/examples/example-unit-test/Cargo.lock b/examples/example-unit-test/Cargo.lock new file mode 100644 index 0000000..712440e --- /dev/null +++ b/examples/example-unit-test/Cargo.lock @@ -0,0 +1,76 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "example-unit-test" +version = "0.1.0" +dependencies = [ + "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", +] + +[[package]] +name = "mingling_core" +version = "0.1.9" +dependencies = [ + "just_fmt", +] + +[[package]] +name = "mingling_macros" +version = "0.1.9" +dependencies = [ + "just_fmt", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "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-unit-test/Cargo.toml b/examples/example-unit-test/Cargo.toml new file mode 100644 index 0000000..4a82503 --- /dev/null +++ b/examples/example-unit-test/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "example-unit-test" +version = "0.1.0" +edition = "2024" + +[dependencies] +mingling = { path = "../../mingling" } diff --git a/examples/example-unit-test/src/main.rs b/examples/example-unit-test/src/main.rs new file mode 100644 index 0000000..d0e1b90 --- /dev/null +++ b/examples/example-unit-test/src/main.rs @@ -0,0 +1,122 @@ +//! 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 +//! ``` + +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 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_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(); +} diff --git a/examples/test-examples.toml b/examples/test-examples.toml index a63f566..68285fd 100644 --- a/examples/test-examples.toml +++ b/examples/test-examples.toml @@ -1,17 +1,112 @@ -[[test.example-async]] -command = "hello World" +[[test.example-basic]] +command = "greet" expect.exit-code = 0 expect.result = "Hello, World!" [[test.example-basic]] -command = "hello World" +command = "greet Alice" expect.exit-code = 0 -expect.result = "Hello, World!" +expect.result = "Hello, Alice!" + +[[test.example-exitcode]] +command = "hello Alice" +expect.exit-code = 0 +expect.result = "Hello, Alice" -[[test.example-exit-code]] -command = "error" +[[test.example-exitcode]] +command = "hello" expect.exit-code = 1 -expect.result = "Exit with exit code: 1" +expect.result = "No name provided (with exit code 1)" + +[[test.example-error-handling]] +command = "hello" +expect.exit-code = 0 +expect.result = "No name provided" + +[[test.example-error-handling]] +command = "hello MyBestFriendAlice" +expect.exit-code = 0 +expect.result = "Name too long: 17 > 10" + +[[test.example-error-handling]] +command = "hello Alice" +expect.exit-code = 0 +expect.result = "Name not available" + +[[test.example-error-handling]] +command = "hello Peter" +expect.exit-code = 0 +expect.result = "Hello, Peter" + +[[test.example-error-handling]] +command = "hallo" +expect.exit-code = 0 +expect.result = "Command not found: \"hallo\"" + +[[test.example-argument-parse]] +command = "transfer" +expect.exit-code = 0 +expect.result = "file: (1048576)" + +[[test.example-argument-parse]] +command = "transfer --dir src" +expect.exit-code = 0 +expect.result = "dir: src (1048576)" + +[[test.example-argument-parse]] +command = "transfer --size 500 myfile.txt" +expect.exit-code = 0 +expect.result = "file: myfile.txt (500)" + +[[test.example-argument-parse]] +command = "strict-transfer README.md" +expect.exit-code = 0 +expect.result = "file: README.md (1048576)" + +[[test.example-argument-parse]] +command = "strict-transfer" +expect.exit-code = 0 +expect.result = "Error: name is not provided" + +[[test.example-async-support]] +command = "download readme.txt" +expect.exit-code = 0 +expect.result = "\"readme.txt\" downloaded." + +[[test.example-custom-pickable]] +command = "connect 192.168.1.1:8080" +expect.exit-code = 0 +expect.result = "Connected to \"192.168.1.1:8080\"" + +[[test.example-custom-pickable]] +command = "connect" +expect.exit-code = 0 +expect.result = "Failed to parse address" + +[[test.example-custom-pickable]] +command = "connect invalid" +expect.exit-code = 0 +expect.result = "Failed to parse address" + +[[test.example-dispatch-tree]] +command = "cmd5" +expect.exit-code = 0 +expect.result = "It's works!" + +[[test.example-enum-tag]] +command = "lang-select" +expect.exit-code = 0 +expect.result = "Selected: Rust" + +[[test.example-enum-tag]] +command = "lang-select Python" +expect.exit-code = 0 +expect.result = "Selected: Python" + +[[test.example-enum-tag]] +command = "lang-select OCaml" +expect.exit-code = 0 +expect.result = "Selected: OCaml" [[test.example-general-renderer]] command = "render Bob 22" @@ -23,32 +118,67 @@ command = "render Bob 22 --json" expect.exit-code = 0 expect.result = "{\"member_name\":\"Bob\",\"member_age\":22}" -[[test.example-general-renderer]] -command = "render Bob 22 --yaml" +[[test.example-help]] +command = "greet" +expect.exit-code = 0 +expect.result = "" + +[[test.example-help]] +command = "greet --help" expect.exit-code = 0 -expect.result = "member_name: Bob\nmember_age: 22" +expect.result = "Usage: greet <NAME>" -[[test.example-picker]] -command = "pick Bob" +[[test.example-hook]] +command = "greet" expect.exit-code = 0 -expect.result = "Picked: name = Bob, age = 20" +expect.result = "Hello, World!" + +[[test.example-hook]] +command = "greet Alice" +expect.exit-code = 0 +expect.result = "Hello, Alice!" + +[[test.example-panic-unwind]] +command = "panic" +expect.exit-code = 0 +expect.result = "Program not panic" + +[[test.example-panic-unwind]] +command = "panic something_went_wrong" +expect.exit-code = 0 +expect.result = "Program panic: something_went_wrong" -[[test.example-picker]] -command = "pick Bob --age -15" +[[test.example-resources]] +command = "current" expect.exit-code = 0 -expect.result = "Picked: name = Bob, age = 0" +expect.result = "Current directory:" -[[test.example-picker]] -command = "pick --age 99" +[[test.example-setup]] +command = "1" expect.exit-code = 0 -expect.result = "No name provided." +expect.result = "" -[[test.example-picker]] -command = "pick" +[[test.example-setup]] +command = "2" expect.exit-code = 0 -expect.result = "No name provided." +expect.result = "" -[[test.example-picker]] -command = "pick --age 150" +[[test.example-setup]] +command = "3" expect.exit-code = 0 -expect.result = "No name provided." +expect.result = "" + +[[test.example-setup]] +command = "4" +expect.exit-code = 0 +expect.result = "" + +[[test.example-setup]] +command = "5" +expect.exit-code = 0 +expect.result = "" + +[[test.example-completion]] +command = "greet World --repeat 1" +expect.exit-code = 0 +expect.result = "Hello, World!" |
