From f5cdf5cc7c3bd434ff7a88c73b33f96c4d3b6562 Mon Sep 17 00:00:00 2001 From: Weicao-CatilGrass <1992414357@qq.com> Date: Sat, 9 May 2026 14:31:42 +0800 Subject: Add CI tooling and cargo alias `ci` --- .cargo/config.toml | 3 + dev_tools/Cargo.lock | 25 +++ dev_tools/Cargo.toml | 1 + dev_tools/src/bin/ci.rs | 95 ++++++++++ dev_tools/src/bin/docs-code-box-fix.rs | 13 +- dev_tools/src/bin/docsify-sidebar-gen.rs | 8 +- dev_tools/src/bin/refresh-docs.rs | 30 ++- dev_tools/src/lib.rs | 102 +++++++++++ docs/_zh_CN/pages/2-implementing-fallbacks.md | 2 +- docs/_zh_CN/pages/3-parsing-complex-arguments.md | 40 ++-- docs/pages/1-creating-your-first-program.md | 2 +- docs/pages/3-parsing-complex-arguments.md | 90 ++++----- docs/res/ci_banner.txt | 12 ++ mingling/src/example_docs.rs | 222 +++++++++++------------ mingling/src/lib.rs | 2 +- 15 files changed, 455 insertions(+), 192 deletions(-) create mode 100644 dev_tools/src/bin/ci.rs create mode 100644 docs/res/ci_banner.txt diff --git a/.cargo/config.toml b/.cargo/config.toml index e3cd902..42e41f8 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -2,3 +2,6 @@ target-dir = "./.temp/target" [env] + +[alias] +ci = "run --manifest-path dev_tools/Cargo.toml --bin ci --quiet" diff --git a/dev_tools/Cargo.lock b/dev_tools/Cargo.lock index 39a1521..7bcc602 100644 --- a/dev_tools/Cargo.lock +++ b/dev_tools/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys", +] + [[package]] name = "just_fmt" version = "0.1.2" @@ -21,6 +30,22 @@ dependencies = [ name = "tools" version = "0.1.0" dependencies = [ + "colored", "just_fmt", "just_template", ] + +[[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/dev_tools/Cargo.toml b/dev_tools/Cargo.toml index a28b156..7abb157 100644 --- a/dev_tools/Cargo.toml +++ b/dev_tools/Cargo.toml @@ -6,3 +6,4 @@ edition = "2024" [dependencies] just_template = "0.1.3" just_fmt = "0.1.2" +colored = "3.1.1" diff --git a/dev_tools/src/bin/ci.rs b/dev_tools/src/bin/ci.rs new file mode 100644 index 0000000..d5a108e --- /dev/null +++ b/dev_tools/src/bin/ci.rs @@ -0,0 +1,95 @@ +use std::process::exit; + +use tools::{cargo_tomls, eprintln_cargo_style, println_cargo_style, run_cmd}; + +fn main() { + #[cfg(windows)] + let _ = colored::control::set_virtual_terminal(true); + println!("{}", include_str!("../../../docs/res/ci_banner.txt")); + + let needs_commit_temp = !{ run_cmd!("git diff-index --quiet HEAD --").is_ok() }; + + if needs_commit_temp { + run_cmd!("git add .").unwrap(); + run_cmd!("git commit -m \"CI Temp\"").unwrap(); + } + + if ci().is_ok() { + println_cargo_style!("Done: All check passed!") + } + + let is_worktree_clean = run_cmd!("git diff-index --quiet HEAD --").is_ok(); + if !is_worktree_clean { + eprintln_cargo_style!("Documents needs refresh!"); + if needs_commit_temp { + run_cmd!("git restore .").unwrap(); + run_cmd!("git reset --soft HEAD~1").unwrap(); + } + exit(1) + } + + if needs_commit_temp { + run_cmd!("git restore .").unwrap(); + run_cmd!("git reset --soft HEAD~1").unwrap(); + } +} + +fn ci() -> Result<(), i32> { + build_all()?; + clippy_all()?; + test_all()?; + docs_refresh()?; + + run_cmd!("git add --renormalize .")?; + + Ok(()) +} + +fn build_all() -> Result<(), i32> { + let cargo_tomls = cargo_tomls(); + for cargo_toml in cargo_tomls { + println_cargo_style!("Build: {}", cargo_toml.to_string_lossy()); + run_cmd!( + "cargo check --manifest-path {}", + cargo_toml.to_string_lossy() + )?; + } + + Ok(()) +} + +fn clippy_all() -> Result<(), i32> { + let cargo_tomls = cargo_tomls(); + for cargo_toml in cargo_tomls { + println_cargo_style!("Clippy: {}", cargo_toml.to_string_lossy()); + run_cmd!( + "cargo clippy --manifest-path {} -- -D warnings", + cargo_toml.to_string_lossy() + )?; + } + + Ok(()) +} + +fn test_all() -> Result<(), i32> { + let cargo_tomls = cargo_tomls(); + for cargo_toml in cargo_tomls { + println_cargo_style!("Testing: {}", cargo_toml.to_string_lossy()); + run_cmd!( + "cargo test --manifest-path {}", + cargo_toml.to_string_lossy() + )?; + } + + Ok(()) +} + +fn docs_refresh() -> Result<(), i32> { + println_cargo_style!("Refresh: document at `./docs/`"); + + run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --bin docs-code-box-fix")?; + run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --bin docsify-sidebar-gen")?; + run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --bin refresh-docs")?; + + Ok(()) +} diff --git a/dev_tools/src/bin/docs-code-box-fix.rs b/dev_tools/src/bin/docs-code-box-fix.rs index db97592..21d2cce 100644 --- a/dev_tools/src/bin/docs-code-box-fix.rs +++ b/dev_tools/src/bin/docs-code-box-fix.rs @@ -1,6 +1,8 @@ use std::fs; use std::path::Path; +use tools::println_cargo_style; + /// Docsify code blocks require that blank lines before and after code blocks are not completely empty, /// but must contain at least one space, otherwise code block rendering will have issues. /// @@ -9,7 +11,7 @@ use std::path::Path; const DOCS_DIR: &str = "./docs"; fn main() { - println!("Fixing code box empty lines in docs/**/*.md ..."); + println_cargo_style!("Fixing: code box empty lines in docs/**/*.md ..."); let repo_root = find_git_repo().expect("Cannot find git repo root"); let docs_dir = repo_root.join(DOCS_DIR); @@ -32,15 +34,16 @@ fn main() { let new_content = fix_code_box_empty_lines(&content); if new_content != content { fs::write(path, &new_content).unwrap(); - println!(" Fixed: {}", path.display()); + println_cargo_style!("Fixed: {}", path.display()); fixed_count += 1; } file_count += 1; }); - println!( - "Done. Scanned {} files, fixed {} files.", - file_count, fixed_count + println_cargo_style!( + "Done: Scanned {} files, fixed {} files.", + file_count, + fixed_count ); } diff --git a/dev_tools/src/bin/docsify-sidebar-gen.rs b/dev_tools/src/bin/docsify-sidebar-gen.rs index ed9e9f0..e0f9370 100644 --- a/dev_tools/src/bin/docsify-sidebar-gen.rs +++ b/dev_tools/src/bin/docsify-sidebar-gen.rs @@ -1,13 +1,15 @@ use std::collections::BTreeMap; use std::path::Path; +use tools::println_cargo_style; + const PAGES_ROOT: &str = "./docs/pages"; const SIDEBAR_PATH: &str = "./docs/_sidebar.md"; const SIDEBAR_HEAD: &str = "- [Welcome!](README)\n"; fn main() { - println!("Refreshing _sidebar.md"); + println_cargo_style!("Refresh: _sidebar.md"); gen_sidebar(); gen_translation_sidebars(); } @@ -21,7 +23,7 @@ fn gen_sidebar() { let sidebar_path = repo_root.join(SIDEBAR_PATH); std::fs::write(&sidebar_path, lines).unwrap(); - println!(" Generated: {}", sidebar_path.display()); + println_cargo_style!("Generated: {}", sidebar_path.display()); } /// Generate _sidebar.md inside translation directories @@ -48,7 +50,7 @@ fn gen_translation_sidebars() { let sidebar_path = path.join("_sidebar.md"); std::fs::write(&sidebar_path, lines).unwrap(); - println!(" Generated: {}", sidebar_path.display()); + println_cargo_style!("Generated: {}", sidebar_path.display()); } } } diff --git a/dev_tools/src/bin/refresh-docs.rs b/dev_tools/src/bin/refresh-docs.rs index 32821ed..ffa80a2 100644 --- a/dev_tools/src/bin/refresh-docs.rs +++ b/dev_tools/src/bin/refresh-docs.rs @@ -2,6 +2,7 @@ use std::path::Path; use just_fmt::snake_case; use just_template::{Template, tmpl}; +use tools::println_cargo_style; const EXAMPLE_ROOT: &str = "./examples/"; const OUTPUT_PATH: &str = "./mingling/src/example_docs.rs"; @@ -9,10 +10,7 @@ const OUTPUT_PATH: &str = "./mingling/src/example_docs.rs"; const TEMPLATE_CONTENT: &str = include_str!("../../../mingling/src/example_docs.rs.tmpl"); fn main() { - { - println!("Refreshing Examples"); - gen_example_doc_module(); - } + gen_example_doc_module(); } fn gen_example_doc_module() { @@ -32,6 +30,8 @@ fn gen_example_doc_module() { } } + examples.sort(); + for example in examples { tmpl!(template += { examples { @@ -43,7 +43,7 @@ fn gen_example_doc_module() { ) } }); - println!(" Refresh: {}", example.name); + println_cargo_style!("Refresh: {}", example.name); } let template_str = template.to_string(); @@ -63,6 +63,26 @@ struct ExampleContent { cargo_toml: String, } +impl PartialOrd for ExampleContent { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ExampleContent { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.name.cmp(&other.name) + } +} + +impl PartialEq for ExampleContent { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} + +impl Eq for ExampleContent {} + impl ExampleContent { pub fn read(name: &str) -> Self { let repo = find_git_repo().unwrap(); diff --git a/dev_tools/src/lib.rs b/dev_tools/src/lib.rs index 8b13789..1b5dd0f 100644 --- a/dev_tools/src/lib.rs +++ b/dev_tools/src/lib.rs @@ -1 +1,103 @@ +use colored::Colorize; +#[macro_export] +macro_rules! run_cmd { + ($fmt:literal, $($arg:tt)*) => { + $crate::run_cmd(format!($fmt, $($arg)*)) + }; + ($cmd:expr) => { + $crate::run_cmd($cmd) + }; +} + +#[macro_export] +macro_rules! println_cargo_style { + ($fmt:literal, $($arg:tt)*) => { + $crate::println_cargo_style(format!($fmt, $($arg)*)) + }; + ($cmd:expr) => { + $crate::println_cargo_style($cmd) + }; +} + +#[macro_export] +macro_rules! eprintln_cargo_style { + ($fmt:literal, $($arg:tt)*) => { + $crate::eprintln_cargo_style(format!($fmt, $($arg)*)) + }; + ($cmd:expr) => { + $crate::eprintln_cargo_style($cmd) + }; +} + +pub fn println_cargo_style(str: impl Into) { + let s = str.into(); + let (prefix, content) = if let Some(pos) = s.find(':') { + ( + s[..pos].trim().to_string(), + s[pos + 1..].trim_start().to_string(), + ) + } else { + ("".to_string(), s.trim().to_string()) + }; + + if prefix.len() > 12 { + panic!( + "prefix length exceeds 12: '{}' has length {}", + prefix, + prefix.len() + ); + } + + let padding = " ".repeat(12 - prefix.len()); + + println!( + "{}{} {}", + padding, + prefix.bold().bright_green(), + content.trim() + ); +} + +pub fn eprintln_cargo_style(str: impl Into) { + println!("{}: {}", "error".bold().bright_red(), str.into()); +} + +pub fn run_cmd(cmd: impl Into) -> Result<(), i32> { + let shell = if cfg!(target_os = "windows") { + "powershell" + } else { + "sh" + }; + let status = std::process::Command::new(shell) + .arg("-c") + .arg(cmd.into()) + .current_dir(std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))) + .status() + .expect("failed to execute command"); + + let exit_code = status.code().unwrap_or(1); + if exit_code == 0 { + Ok(()) + } else { + Err(exit_code) + } +} + +pub fn cargo_tomls() -> Vec { + let mut cargo_tomls = Vec::new(); + let mut dirs = vec![std::path::PathBuf::from(".")]; + while let Some(dir) = dirs.pop() { + if let Ok(entries) = std::fs::read_dir(&dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + dirs.push(path); + } else if path.file_name().and_then(|n| n.to_str()) == Some("Cargo.toml") { + cargo_tomls.push(path); + } + } + } + } + cargo_tomls +} diff --git a/docs/_zh_CN/pages/2-implementing-fallbacks.md b/docs/_zh_CN/pages/2-implementing-fallbacks.md index a7c04d0..e4fd3f8 100644 --- a/docs/_zh_CN/pages/2-implementing-fallbacks.md +++ b/docs/_zh_CN/pages/2-implementing-fallbacks.md @@ -133,7 +133,7 @@ thread 'main' (90772) panicked at src/bin/your-bin.rs:30:5: Renderer "ResultGreetSomeone" not found! note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ``` - + 以上便是 **Mingling** 的回退机制,在接下来的章节中,您将学习如何使用 `Picker` 解析复杂的用户输入。

diff --git a/docs/_zh_CN/pages/3-parsing-complex-arguments.md b/docs/_zh_CN/pages/3-parsing-complex-arguments.md index 923218e..4ee9cec 100644 --- a/docs/_zh_CN/pages/3-parsing-complex-arguments.md +++ b/docs/_zh_CN/pages/3-parsing-complex-arguments.md @@ -12,7 +12,7 @@ ```rust let name = args.first().cloned().unwrap_or_else(|| "World".to_string()); ``` - + 而本章节将会引入新的 **Mingling** 特性:`Picker`,它提供轻量且和 **Mingling** 类型路由高度契合的命令解析方案。 要启用 `Picker`,您需要修改 `Cargo.toml` ✏️ @@ -24,7 +24,7 @@ mingling = { features = ["parser"] } ``` - + 好了,多的不说,让我们上手编辑代码,重写前文的解析代码 ✏️ ```rust @@ -40,7 +40,7 @@ fn handle_greet_entry(prev: GreetEntry) -> NextProcess { ResultGreetSomeone::new(name) } ``` - + `Picker` 为所有 `Into>` 实现了 `pick` `pick_or` `pick_or_route` 函数:它们可以语义化地从字符串列表中 **拾取 (Pick)** 参数,并转换为结构化数据。 对于上述示例中的代码: @@ -48,7 +48,7 @@ fn handle_greet_entry(prev: GreetEntry) -> NextProcess { ```rust prev.pick_or((), "World").unpack(); ``` - + 它的语义为: ```rust @@ -60,7 +60,7 @@ prev.pick_or((), "World").unpack(); // | |______________________ 拾取或使用默认 // |___________________________ 从前一个输入中 ``` - + ## 解析标志参数 若您的程序设计需要解析标志参数 (例如:`greet --name Alice`),可以使用如下方式: @@ -68,7 +68,7 @@ prev.pick_or((), "World").unpack(); ```rust prev.pick_or(["--name", "-n"], "World").unpack(); ``` - + 同理,它的语义为: ```rust @@ -80,7 +80,7 @@ prev.pick_or(["--name", "-n"], "World").unpack(); // | |____________________________________ 拾取或使用默认 // |_________________________________________ 从前一个输入中 ``` - + ## 关于 `.unpack()` 💡 您可能注意到了,`Picker` 在命令解析的最后,会执行一个 `.unpack()` 函数,它的作用是将前面解析出来的结果,转换为结构化信息。 @@ -94,10 +94,10 @@ let (name, age, id) = prev .pick::(["--age", "-a"]) .pick::(["--id", "-I"]) .unpack(); - + // 可解析参数 --name Alice --age 21 --id 0711251 ``` - + > [!IMPORTANT] > `Picker` 对解析顺序极其敏感,特别是位置参数:因为它是顺序解析的 > @@ -146,7 +146,7 @@ fn render_greet_someone(prev: ResultGreetSomeone) { r_println!("Hello, {}!", *prev); } ``` - + 若使用 `pick_or_route`,写法会变得相对复杂:因为 `.unpack()` 不再直接返回参数,而是 `Result` 不过 **Mingling** 提供了简化展开的宏 `route!`,它不复杂,只是省略了一部分样板代码: @@ -160,7 +160,7 @@ let name = match pick_result { Err(e) => return e, }; ``` - + ## 提取值的后处理 在您使用 `pick` 提取了用户输入后,可以使用 `after` 或 `after_or_route` 立刻处理该参数 ✏️ @@ -182,7 +182,7 @@ fn handle_greet_entry(prev: GreetEntry) -> NextProcess { ResultGreetSomeone::new(name) // 此处传入的 name 已被格式化处理 } ``` - + 同样,您可以使用 `after_or_route` 来处理输入参数的格式错误 ✏️ ```rust @@ -227,7 +227,7 @@ fn render_greet_someone(prev: ResultGreetSomeone) { r_println!("Hello, {}!", *prev); } ``` - + ## 布尔值解析 `Picker` 当然也可以解析 **布尔类型**,但是布尔类型分为显式和隐式模式, @@ -251,7 +251,7 @@ fn handle_some_entry(prev: SomeEntry) -> NextProcess { // 其他逻辑 } ``` - + ## 特殊用法:`usize` 解析 **Mingling** 为 `usize` 提供了一个特殊的用法:解析类似 `25G`、`32mb` 等字样 ✏️ @@ -264,7 +264,7 @@ fn parse_size() { assert_eq!(size, 25 * 1024 * 1024); } ``` - + ## 自定义可解析类型 您可以使用 `Pickable` trait 使您的类型支持被 `Picker` 解析,这也是 `Picker` 拓展性的来源 ✏️ @@ -292,7 +292,7 @@ impl Pickable for Address { } } ``` - + 我们为 `Address` 实现 `Pickable`:接下来我们便可以使用 `ip:port` 的方式来输入参数了 ✏️ ```rust @@ -312,14 +312,14 @@ fn render_connected(prev: ResultConnected) { r_println!("Connected: IP: {} PORT: {}", addr.ip, addr.port); } ``` - + 执行效果如下: ```bash ~> your-bin connect --addr 127.0.0.1:8080 Connected: IP: 127.0.0.1 PORT: 8080 ``` - + ## 自动为枚举实现 Pickable 要为枚举类型实现 `Pickable` trait,无需手动实现:`Picker` 会为所有实现了 `PickableEnum` 的类型实现 `Pickable`,只需要该枚举类型实现了 `EnumTag` ✏️ @@ -339,7 +339,7 @@ pub enum Fruits { // 为 Fruits 实现 PickableEnum impl PickableEnum for Fruits {} ``` - + 接下来您便可以直接使用 `Picker` 解析该类型 ✏️ ```rust @@ -356,7 +356,7 @@ fn render_ate_fruit(prev: ResultFruit) { r_println!("Picked fruit: {:?}", *prev); } ``` - + 以上便是 `Picker` 的所有用法,在下一章节,我会介绍如何在 **Mingling** 内为命令实现帮助文档。

diff --git a/docs/pages/1-creating-your-first-program.md b/docs/pages/1-creating-your-first-program.md index 40690c7..0a6ff3c 100644 --- a/docs/pages/1-creating-your-first-program.md +++ b/docs/pages/1-creating-your-first-program.md @@ -250,7 +250,7 @@ Hello, World! ~> your-bin greet Alice Hello, Alice! ``` - + At this point, you have successfully created a basic **Mingling** command-line program. The next chapter will explain how to implement a fallback mechanism for your command-line program to handle cases where a command or renderer does not exist.

diff --git a/docs/pages/3-parsing-complex-arguments.md b/docs/pages/3-parsing-complex-arguments.md index 8cd5503..b48b28b 100644 --- a/docs/pages/3-parsing-complex-arguments.md +++ b/docs/pages/3-parsing-complex-arguments.md @@ -12,7 +12,7 @@ ```rust let name = args.first().cloned().unwrap_or_else(|| "World".to_string()); ``` - + This chapter introduces a new **Mingling** feature: `Picker`. It provides a lightweight parsing solution that meshes well with **Mingling**'s typed routing. To enable `Picker`, edit `Cargo.toml` ✏️ @@ -24,7 +24,7 @@ mingling = { features = ["parser"] } ``` - + Enough talk, let's get coding and rewrite the parsing logic from the prev. section ✏️ ```rust @@ -33,14 +33,14 @@ fn handle_greet_entry(prev: GreetEntry) -> NextProcess { // Prev. approach: // let args = prev.inner; // let name = args.first().cloned().unwrap_or_else(|| "World".to_string()); - + // New approach with Picker let name = prev.pick_or((), "World").unpack(); - + ResultGreetSomeone::new(name) } ``` - + `Picker` implements `pick`, `pick_or`, and `pick_or_route` for anything `Into>`. These functions let you semantically **pick** args from a string list and convert them into structured data. In the code above: @@ -48,7 +48,7 @@ fn handle_greet_entry(prev: GreetEntry) -> NextProcess { ```rust prev.pick_or((), "World").unpack(); ``` - + Its meaning: ```rust @@ -60,7 +60,7 @@ prev.pick_or((), "World").unpack(); // | |______________________ pick or use default // |___________________________ from the prev. input ``` - + ## Parsing Flag Args If your app needs to parse flag args (e.g., `greet --name Alice`), do: @@ -68,7 +68,7 @@ prev.pick_or((), "World").unpack(); ```rust prev.pick_or(["--name", "-n"], "World").unpack(); ``` - + Its meaning: ```rust @@ -80,7 +80,7 @@ prev.pick_or(["--name", "-n"], "World").unpack(); // | |____________________________________ pick or use default // |_________________________________________ from the prev. input ``` - + ## About `.unpack()` 💡 You may have noticed `Picker` calls `.unpack()` at the end of parsing. It converts the parsed result into structured info. @@ -94,10 +94,10 @@ let (name, age, id) = prev .pick::(["--age", "-a"]) .pick::(["--id", "-I"]) .unpack(); - + // Parses: --name Alice --age 21 --id 0711251 ``` - + > [!IMPORTANT] > `Picker` is very order-sensitive, esp. with positional args: it parses sequentially. > @@ -113,10 +113,10 @@ let (name, age, id) = prev ```rust dispatcher!("greet", GreetCommand => GreetEntry); - + pack!(ResultGreetSomeone = String); pack!(ErrorGreetNoNameProvided = ()); - + #[chain] fn handle_greet_entry(prev: GreetEntry) -> NextProcess { // Use `pick_or_route` to extract the `--name` arg @@ -128,39 +128,39 @@ fn handle_greet_entry(prev: GreetEntry) -> NextProcess { ) // After using any routable method, `unpack` returns `Result` .unpack(); - + // Use the `route!` macro to expand `pick_result`, // If it's `Err`, the chain returns here, routing to the specified type let name = route!(pick_result); ResultGreetSomeone::new(name).to_chain() } - + // Handles rendering for `ErrorGreetNoNameProvided` #[renderer] fn render_err_greet_no_name_provided(_prev: ErrorGreetNoNameProvided) { r_println!("Error: No name provided.") } - + #[renderer] fn render_greet_someone(prev: ResultGreetSomeone) { r_println!("Hello, {}!", *prev); } ``` - + Using `pick_or_route` makes the code a bit more complex: `.unpack()` no longer returns the value directly, but `Result`. However, **Mingling** provides the `route!` macro to simplify expansion. It's not complex—just cuts some boilerplate: ```rust let name = route!(pick_result); - + // Expands to let name = match pick_result { Ok(r) => r, Err(e) => return e, }; ``` - + ## Post-Processing Extracted Values After using `pick` to extract user input, you can use `after` or `after_or_route` to process the arg immediately ✏️ @@ -178,19 +178,19 @@ fn handle_greet_entry(prev: GreetEntry) -> NextProcess { .to_string() }) .unpack(); - + ResultGreetSomeone::new(name) // name is now formatted } ``` - + Similarly, use `after_or_route` to handle format errors in input args ✏️ ```rust dispatcher!("greet", GreetCommand => GreetEntry); - + pack!(ResultGreetSomeone = String); pack!(ErrorGreetNameTooLong = usize); - + #[chain] fn handle_greet_entry(prev: GreetEntry) -> NextProcess { let pick_result = prev @@ -201,7 +201,7 @@ fn handle_greet_entry(prev: GreetEntry) -> NextProcess { .to_lowercase() .trim() .to_string(); - + // Check name length, route to error type if too long let len = name.len(); if len < 32 { @@ -212,22 +212,22 @@ fn handle_greet_entry(prev: GreetEntry) -> NextProcess { }) .unpack(); let name = route!(pick_result); - + ResultGreetSomeone::new(name).to_chain() } - + #[renderer] fn render_error_greet_name_too_long(prev: ErrorGreetNameTooLong) { let len = *prev; r_println!("Error: name too long (length: {} > 32)", len); } - + #[renderer] fn render_greet_someone(prev: ResultGreetSomeone) { r_println!("Hello, {}!", *prev); } ``` - + ## Parsing Booleans `Picker` can parse **bool** types too, but with both explicit and implicit modes: @@ -247,11 +247,11 @@ fn render_greet_someone(prev: ResultGreetSomeone) { fn handle_some_entry(prev: SomeEntry) -> NextProcess { let confirmed: bool = prev.pick::(()).unpack().is_yes(); let confirm: bool = prev.pick::(["--confirm", "-C"]).unpack(); - + // other logic } ``` - + ## Special Use: `usize` Parsing **Mingling** has a special use for `usize`: parsing strings like `25G`, `32mb`, etc. ✏️ @@ -264,7 +264,7 @@ fn parse_size() { assert_eq!(size, 25 * 1024 * 1024); } ``` - + ## Custom Parsable Types Use the `Pickable` trait to make your types parsable by `Picker`. This is where `Picker`'s extensibility comes from ✏️ @@ -276,50 +276,50 @@ pub struct Address { ip: String, port: u16, } - + impl Pickable for Address { type Output = Self; fn pick(args: &mut Argument, flag: Flag) -> Option { // Extract raw string from Argument using Flag let raw = args.pick_argument(flag)?; - + // Parse raw string into structured data let parts: Vec<&str> = raw.split(':').collect(); let ip = parts.first()?.to_string(); let port: u16 = parts.get(1)?.parse().ok()?; - + Some(Address { ip, port }) } } ``` - + With `Pickable` implemented for `Address`, we can now use `ip:port` format for input ✏️ ```rust dispatcher!("connect", ConnectCommand => ConnectEntry); - + pack!(ResultConnected = Address); - + #[chain] fn handle_connect_entry(prev: ConnectEntry) -> NextProcess { let address: Address = prev.pick("--addr").unpack(); ResultConnected::new(address) } - + #[renderer] fn render_connected(prev: ResultConnected) { let addr = prev.inner; r_println!("Connected: IP: {} PORT: {}", addr.ip, addr.port); } ``` - + Running it: ```bash ~> your-bin connect --addr 127.0.0.1:8080 Connected: IP: 127.0.0.1 PORT: 8080 ``` - + ## Auto-Implementing Pickable for Enums No need to manually implement `Pickable` for enums: `Picker` auto-implements it for any type that implements `PickableEnum`, as long as it also implements `EnumTag` ✏️ @@ -335,28 +335,28 @@ pub enum Fruits { Banana, Orange, } - + // Implement PickableEnum for Fruits impl PickableEnum for Fruits {} ``` - + Now you can directly use `Picker` to parse this type ✏️ ```rust pack!(ResultFruit = Fruits); - + #[chain] fn handle_eat_fruit_entry(prev: EatFruitEntry) -> NextProcess { let fruit: Fruits = prev.pick("--fruit").unpack(); ResultFruit::new(fruit) } - + #[renderer] fn render_ate_fruit(prev: ResultFruit) { r_println!("Picked fruit: {:?}", *prev); } ``` - + That's all for `Picker`'s usage. In the next chapter, I'll introduce how to implement help docs for commands in **Mingling**.

diff --git a/docs/res/ci_banner.txt b/docs/res/ci_banner.txt new file mode 100644 index 0000000..78f574b --- /dev/null +++ b/docs/res/ci_banner.txt @@ -0,0 +1,12 @@ + __ __ __ __ __ ______ ______ + / \ / |/ | / | / | / \ / | + ██ \ /██ |██/ _______ ______ ██ | ██/ _______ ______ /██████ |██████/ + ███ \ /███ |/ |/ \ / \ ██ | / |/ \ / \ ██ | ██/ ██ | + ████ /████ |██ |███████ |/██████ | ██ | ██ |███████ |/██████ | ██ | ██ | + ██ ██ ██/██ |██ |██ | ██ |██ | ██ | ██ | ██ |██ | ██ |██ | ██ | ██ | __ ██ | + ██ |███/ ██ |██ |██ | ██ |██ \__██ | ██ |_____ ██ |██ | ██ |██ \__██ | ██ \__/ | _██ |_ + ██ | █/ ██ |██ |██ | ██ |██ ██ | ██ |██ |██ | ██ |██ ██ | ██ ██/ / ██ | + ██/ ██/ ██/ ██/ ██/ ███████ | ████████/ ██/ ██/ ██/ ███████ | ██████/ ██████/ + / \__██ | / \__██ | + ██ ██/ ██ ██/ + ██████/ ██████/ diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs index aca27ce..9f1b46b 100644 --- a/mingling/src/example_docs.rs +++ b/mingling/src/example_docs.rs @@ -1,133 +1,133 @@ // Auto generated -/// `Mingling` Example - Basic +/// `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-basic/Cargo.toml -- hello World +/// cargo run --manifest-path ./examples/example-async/Cargo.toml -- hello World /// ``` /// /// Cargo.toml /// ```ignore /// [package] -/// name = "example-basic" +/// name = "example-async" /// version = "0.0.1" /// edition = "2024" /// /// [dependencies] -/// mingling = { path = "../../mingling" } +/// tokio = { version = "1", features = ["full"] } +/// mingling = { path = "../../mingling", features = ["async"] } /// ``` /// /// main.rs /// ```ignore /// use mingling::macros::{chain, dispatcher, gen_program, pack, r_println, renderer}; /// -/// // Define dispatcher `HelloCommand`, directing subcommand "hello" to `HelloEntry` /// dispatcher!("hello", HelloCommand => HelloEntry); /// -/// fn main() { -/// // Create program +/// // Use Tokio async runtime +/// #[tokio::main] +/// async fn main() { /// let mut program = ThisProgram::new(); -/// -/// // Add dispatcher `HelloCommand` /// program.with_dispatcher(HelloCommand); /// /// // Run program -/// program.exec(); +/// program.exec().await; /// } /// -/// // Register wrapper type `Hello`, setting inner to `String` /// pack!(Hello = String); /// -/// // Register chain to `ThisProgram`, handling logic from `HelloEntry` +/// // You can freely use async / non-async functions to declare your Chain +/// /// #[chain] -/// fn parse_name(prev: HelloEntry) -> NextProcess { -/// // Extract string from `HelloEntry` as argument +/// // fn parse_name(prev: HelloEntry) -> NextProcess { +/// async fn parse_name(prev: HelloEntry) -> NextProcess { /// let name = prev.first().cloned().unwrap_or_else(|| "World".to_string()); -/// -/// // Build `Hello` type and route to renderer /// Hello::new(name).to_render() /// } /// -/// // Register renderer to `ThisProgram`, handling rendering of `Hello` +/// // For renderers, you can still only use synchronous functions /// #[renderer] /// fn render_hello_who(prev: Hello) { -/// // Print message /// r_println!("Hello, {}!", *prev); -/// -/// // Program ends here /// } /// -/// // Generate program, default is `ThisProgram` /// gen_program!(); /// ``` -pub mod example_basic {} -/// `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"] } -/// ``` +pub mod example_async {} +/// `Mingling` Example - Basic /// /// # How to Run /// ```bash -/// cargo run --manifest-path ./examples/example-async/Cargo.toml -- hello World +/// cargo run --manifest-path ./examples/example-basic/Cargo.toml -- hello World /// ``` /// /// Cargo.toml /// ```ignore /// [package] -/// name = "example-async" +/// name = "example-basic" /// version = "0.0.1" /// edition = "2024" /// /// [dependencies] -/// tokio = { version = "1", features = ["full"] } -/// mingling = { path = "../../mingling", features = ["async"] } +/// mingling = { path = "../../mingling" } /// ``` /// /// main.rs /// ```ignore /// use mingling::macros::{chain, dispatcher, gen_program, pack, r_println, renderer}; /// +/// // Define dispatcher `HelloCommand`, directing subcommand "hello" to `HelloEntry` /// dispatcher!("hello", HelloCommand => HelloEntry); /// -/// // Use Tokio async runtime -/// #[tokio::main] -/// async fn main() { +/// fn main() { +/// // Create program /// let mut program = ThisProgram::new(); +/// +/// // Add dispatcher `HelloCommand` /// program.with_dispatcher(HelloCommand); /// /// // Run program -/// program.exec().await; +/// program.exec(); /// } /// +/// // Register wrapper type `Hello`, setting inner to `String` /// pack!(Hello = String); /// -/// // You can freely use async / non-async functions to declare your Chain -/// +/// // Register chain to `ThisProgram`, handling logic from `HelloEntry` /// #[chain] -/// // fn parse_name(prev: HelloEntry) -> NextProcess { -/// async fn parse_name(prev: HelloEntry) -> NextProcess { +/// fn parse_name(prev: HelloEntry) -> NextProcess { +/// // 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() /// } /// -/// // For renderers, you can still only use synchronous functions +/// // Register renderer to `ThisProgram`, handling rendering of `Hello` /// #[renderer] /// fn render_hello_who(prev: Hello) { +/// // Print message /// r_println!("Hello, {}!", *prev); +/// +/// // Program ends here /// } /// +/// // Generate program, default is `ThisProgram` /// gen_program!(); /// ``` -pub mod example_async {} +pub mod example_basic {} /// `Mingling` Example - Completion /// /// # How to Deploy @@ -270,6 +270,69 @@ pub mod example_async {} /// gen_program!(); /// ``` pub mod example_completion {} +/// `Mingling` Example - Dispatch Tree +/// +/// # How to Deploy +/// 1. Enable the `dispatch_tree` feature (`comp` is optional) +/// ```toml +/// mingling = { version = "...", features = [ +/// "dispatch_tree", // Enable this feature +/// "comp" // optional +/// ] } +/// ``` +/// +/// 2. Using `cargo expand`: +/// +/// ```bash +/// cargo expand --manifest-path examples/example-dispatch-tree/Cargo.toml > expanded.rs +/// cat expanded.rs | grep dispatch_args_trie -A 264 +/// ``` +/// +/// Cargo.toml +/// ```ignore +/// [package] +/// name = "example-dispatch-tree" +/// version = "0.1.0" +/// edition = "2024" +/// +/// [dependencies] +/// mingling = { path = "../../mingling", features = ["dispatch_tree", "comp"] } +/// ``` +/// +/// main.rs +/// ```ignore +/// #![allow(unused_mut)] +/// +/// use mingling::macros::{dispatcher, gen_program}; +/// +/// fn main() { +/// let mut 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); +/// +/// program.exec(); +/// } +/// +/// 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); +/// +/// gen_program!(); +/// ``` +pub mod example_dispatch_tree {} /// `Mingling` Example - General Renderer /// /// ## Step1 - Enable Feature @@ -447,66 +510,3 @@ pub mod example_general_renderer {} /// gen_program!(); /// ``` pub mod example_picker {} -/// `Mingling` Example - Dispatch Tree -/// -/// # How to Deploy -/// 1. Enable the `dispatch_tree` feature (`comp` is optional) -/// ```toml -/// mingling = { version = "...", features = [ -/// "dispatch_tree", // Enable this feature -/// "comp" // optional -/// ] } -/// ``` -/// -/// 2. Using `cargo expand`: -/// -/// ```bash -/// cargo expand --manifest-path examples/example-dispatch-tree/Cargo.toml > expanded.rs -/// cat expanded.rs | grep dispatch_args_trie -A 264 -/// ``` -/// -/// Cargo.toml -/// ```ignore -/// [package] -/// name = "example-dispatch-tree" -/// version = "0.1.0" -/// edition = "2024" -/// -/// [dependencies] -/// mingling = { path = "../../mingling", features = ["dispatch_tree", "comp"] } -/// ``` -/// -/// main.rs -/// ```ignore -/// #![allow(unused_mut)] -/// -/// use mingling::macros::{dispatcher, gen_program}; -/// -/// fn main() { -/// let mut 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); -/// -/// program.exec(); -/// } -/// -/// 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); -/// -/// gen_program!(); -/// ``` -pub mod example_dispatch_tree {} diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs index 21a848d..79d5b19 100644 --- a/mingling/src/lib.rs +++ b/mingling/src/lib.rs @@ -5,7 +5,7 @@ //! //! # Use //! -//! ```rust +//! ```rust,ignore //! use mingling::macros::{dispatcher, gen_program, r_println, renderer}; //! //! #[tokio::main] -- cgit