diff options
289 files changed, 15733 insertions, 1383 deletions
diff --git a/.cargo/config.toml b/.cargo/config.toml index 0b45ca9..f375b03 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -4,5 +4,5 @@ target-dir = "./.temp/target" [env] [alias] -ci = "run --manifest-path dev_tools/Cargo.toml --bin ci --quiet --" -dev_tool = "run --manifest-path dev_tools/Cargo.toml --quiet --bin " +ci = "run --manifest-path .run/Cargo.toml --bin ci --quiet --" +dev_tool = "run --manifest-path .run/Cargo.toml --quiet --bin " diff --git a/.github/workflows/ci-check-only.yml b/.github/workflows/ci-check-only.yml new file mode 100644 index 0000000..1179ed0 --- /dev/null +++ b/.github/workflows/ci-check-only.yml @@ -0,0 +1,47 @@ +name: CI (Check only) + +on: + push: + branches-ignore: [main] + pull_request: + +jobs: + Check-Codes: + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + + - name: Restore .temp cache + id: cache-temp + uses: actions/cache@v4 + with: + path: .temp + key: ${{ runner.os }}-mingling-temp-${{ hashFiles('Cargo.lock', 'dev_tools/Cargo.toml', 'verified-docs.toml', 'examples/test-examples.toml') }}-${{ hashFiles('dev_tools/src/**/*.rs') }} + restore-keys: | + ${{ runner.os }}-mingling-temp- + + - run: cargo ci --test-codes + + Check-Docs: + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + + - name: Restore .temp cache + id: cache-temp + uses: actions/cache@v4 + with: + path: .temp + key: ${{ runner.os }}-mingling-temp-${{ hashFiles('Cargo.lock', 'dev_tools/Cargo.toml', 'verified-docs.toml', 'examples/test-examples.toml') }}-${{ hashFiles('dev_tools/src/**/*.rs') }} + restore-keys: | + ${{ runner.os }}-mingling-temp- + + - run: cargo ci --test-docs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 441ea47..faa513a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ permissions: contents: write jobs: - ci: + Check-Codes: strategy: matrix: os: [ubuntu-latest, windows-latest] @@ -16,11 +16,41 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions-rust-lang/setup-rust-toolchain@v1 - - run: cargo ci - move-unreleased-tag: + - name: Restore .temp cache + id: cache-temp + uses: actions/cache@v4 + with: + path: .temp + key: ${{ runner.os }}-mingling-temp-${{ hashFiles('Cargo.lock', 'dev_tools/Cargo.toml', 'verified-docs.toml', 'examples/test-examples.toml') }}-${{ hashFiles('dev_tools/src/**/*.rs') }} + restore-keys: | + ${{ runner.os }}-mingling-temp- + + - run: cargo ci --test-codes + + Check-Docs: + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + + - name: Restore .temp cache + id: cache-temp + uses: actions/cache@v4 + with: + path: .temp + key: ${{ runner.os }}-mingling-temp-${{ hashFiles('Cargo.lock', 'dev_tools/Cargo.toml', 'verified-docs.toml', 'examples/test-examples.toml') }}-${{ hashFiles('dev_tools/src/**/*.rs') }} + restore-keys: | + ${{ runner.os }}-mingling-temp- + + - run: cargo ci --test-docs + + Move-Unreleased-Tag: if: github.ref == 'refs/heads/main' - needs: ci + needs: [Check-Codes, Check-Docs] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -29,3 +59,26 @@ jobs: - run: | git tag -f unreleased git push origin unreleased --force + + Deploy-Github-Pages: + needs: [Move-Unreleased-Tag] + permissions: + contents: read + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: "." + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/page.yml b/.github/workflows/page.yml deleted file mode 100644 index 6366262..0000000 --- a/.github/workflows/page.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Deploy static content to Pages - -on: - push: - branches: ["main"] - - workflow_dispatch: - -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Pages - uses: actions/configure-pages@v5 - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: "." - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v5 @@ -6,3 +6,9 @@ dev_tools/scripts/last_check # Drafts __*.md __*/ + +# Fuck +nul + +# Release script +Cargo.toml.bkup diff --git a/.run/.gitignore b/.run/.gitignore new file mode 100644 index 0000000..0dc39b0 --- /dev/null +++ b/.run/.gitignore @@ -0,0 +1,7 @@ +# All temp build artifacts will be stored in this dir +/target + +# If you want to add some Rust crates? Remove this line +# /Cargo.* + +last_check diff --git a/dev_tools/Cargo.lock b/.run/Cargo.lock index bb510bd..bb510bd 100644 --- a/dev_tools/Cargo.lock +++ b/.run/Cargo.lock diff --git a/dev_tools/Cargo.toml b/.run/Cargo.toml index 9855580..283aa47 100644 --- a/dev_tools/Cargo.toml +++ b/.run/Cargo.toml @@ -19,3 +19,5 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["rt-multi-thread", "macros"] } indicatif = "0.18.4" + +[workspace] diff --git a/.run/src/bin/RELEASE-WORKSPACE.rs b/.run/src/bin/RELEASE-WORKSPACE.rs new file mode 100644 index 0000000..8cb4d1b --- /dev/null +++ b/.run/src/bin/RELEASE-WORKSPACE.rs @@ -0,0 +1,145 @@ +use std::path::{Path, PathBuf}; +use std::process; +use tools::dependency_order::{display_dependency_order, find_workspace_root}; +use tools::{ + eprintln_cargo_style, println_cargo_style, run_cmd_capture_with_dir, run_cmd_with_dir, +}; + +/// Read the version from `[workspace.package].version` in the root Cargo.toml. +fn read_workspace_version(workspace_root: &Path) -> Option<String> { + let content = std::fs::read_to_string(workspace_root.join("Cargo.toml")).ok()?; + let value: toml::Value = content.parse().ok()?; + value + .get("workspace")? + .get("package")? + .get("version")? + .as_str() + .map(String::from) +} + +/// Replace `path = "crate_name"` with `version = "VERSION"` in the file content. +fn update_path_to_version(content: &str, crate_name: &str, version: &str) -> String { + let old = format!("path = \"{}\"", crate_name); + let new = format!("version = \"{}\"", version); + content.replace(&old, &new) +} + +/// Restore the backup and abort. +fn abort(toml_path: &Path) -> ! { + let backup_path = toml_path.with_extension("toml.bkup"); + if backup_path.exists() { + if let Err(e) = std::fs::copy(&backup_path, toml_path) { + eprintln_cargo_style!("failed to restore Cargo.toml from backup: {}", e); + } else { + eprintln_cargo_style!("restored Cargo.toml from backup"); + } + } + process::exit(1); +} + +fn main() { + // 1. Compute dependency order + let order = display_dependency_order(); + if order.is_empty() { + eprintln_cargo_style!("could not find workspace root or mingling crates"); + process::exit(1); + } + + // 2. Locate workspace root + let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + let workspace_root = match find_workspace_root(&cwd) { + Some(r) => r, + None => { + eprintln_cargo_style!("could not find workspace root"); + process::exit(1); + } + }; + + // 3. Read current version + let version = match read_workspace_version(&workspace_root) { + Some(v) => v, + None => { + eprintln_cargo_style!("could not read workspace.package.version from Cargo.toml"); + process::exit(1); + } + }; + + println_cargo_style!("Version: detected version {}", version); + + let toml_path = workspace_root.join("Cargo.toml"); + let backup_path = workspace_root.join("Cargo.toml.bkup"); + + // 4. Backup Cargo.toml + if let Err(e) = std::fs::copy(&toml_path, &backup_path) { + eprintln_cargo_style!("failed to backup Cargo.toml: {}", e); + process::exit(1); + } + println_cargo_style!("Backup: created Cargo.toml.bkup"); + + // 5. Iterate crates in dependency order + for crate_name in &order { + let name = match crate_name.to_str() { + Some(n) => n, + None => { + eprintln_cargo_style!("invalid crate path: {}", crate_name.display()); + abort(&toml_path); + } + }; + + let crate_dir = workspace_root.join(crate_name); + + // 5a. Publish (capture output to check for warnings) + println_cargo_style!("Publish: {}", name); + let output = run_cmd_capture_with_dir("cargo publish", &crate_dir); + let output_str = match output { + Ok(o) => o, + Err((code, _)) => { + eprintln_cargo_style!("{} publish failed (exit code {})", name, code); + abort(&toml_path); + } + }; + + // Reject any warnings + let warnings: Vec<&str> = output_str + .lines() + .filter(|l| l.to_lowercase().contains("warning:")) + .collect(); + if !warnings.is_empty() { + for w in &warnings { + eprintln_cargo_style!("warning detected: {}", w); + } + eprintln_cargo_style!( + "{} publish rejected due to {} warning(s)", + name, + warnings.len() + ); + abort(&toml_path); + } + + // 5b. Replace path with version in Cargo.toml + let content = match std::fs::read_to_string(&toml_path) { + Ok(c) => c, + Err(e) => { + eprintln_cargo_style!("failed to read Cargo.toml: {}", e); + abort(&toml_path); + } + }; + let new_content = update_path_to_version(&content, name, &version); + if let Err(e) = std::fs::write(&toml_path, &new_content) { + eprintln_cargo_style!("failed to write Cargo.toml: {}", e); + abort(&toml_path); + } + println_cargo_style!("Dependency: {} path -> version \"{}\"", name, version); + + // 5c. Verify workspace compiles + println_cargo_style!("Check: workspace"); + if let Err(code) = run_cmd_with_dir("cargo check --workspace", &workspace_root) { + eprintln_cargo_style!("cargo check --workspace failed (exit code {})", code); + abort(&toml_path); + } + + println_cargo_style!("Done: `{}` published", name); + } + + println_cargo_style!("Done: all crates published successfully"); +} diff --git a/dev_tools/scripts/build-all.ps1 b/.run/src/bin/build-all.ps1 index 4f35ed8..4f35ed8 100644 --- a/dev_tools/scripts/build-all.ps1 +++ b/.run/src/bin/build-all.ps1 diff --git a/dev_tools/scripts/build-all.sh b/.run/src/bin/build-all.sh index 2036b41..2036b41 100755..100644 --- a/dev_tools/scripts/build-all.sh +++ b/.run/src/bin/build-all.sh diff --git a/dev_tools/src/bin/ci.rs b/.run/src/bin/ci.rs index b138fa1..2527ece 100644 --- a/dev_tools/src/bin/ci.rs +++ b/.run/src/bin/ci.rs @@ -139,12 +139,12 @@ fn ci(test_docs: bool, test_codes: bool, run_all: bool) -> Result<(), i32> { } fn test_examples() -> Result<(), i32> { - run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --color always --bin test-examples") + run_cmd!("cargo run --manifest-path .run/Cargo.toml --color always --bin test-examples") } fn test_docs_code_blocks() -> Result<(), i32> { run_cmd!( - "cargo run --manifest-path dev_tools/Cargo.toml --color always --bin test-all-markdown-code" + "cargo run --manifest-path .run/Cargo.toml --color always --bin test-all-markdown-code" ) } @@ -214,11 +214,12 @@ fn test_all() -> Result<(), i32> { 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")?; - run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --bin refresh-feature-mod")?; - run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --bin sync-examples")?; + run_cmd!("cargo run --manifest-path .run/Cargo.toml --bin docs-code-box-fix")?; + run_cmd!("cargo run --manifest-path .run/Cargo.toml --bin docsify-sidebar-gen")?; + run_cmd!("cargo run --manifest-path .run/Cargo.toml --bin refresh-docs")?; + run_cmd!("cargo run --manifest-path .run/Cargo.toml --bin refresh-feature-mod")?; + run_cmd!("cargo run --manifest-path .run/Cargo.toml --bin sync-examples")?; + run_cmd!("cargo fmt")?; Ok(()) } diff --git a/dev_tools/scripts/clippy-fix.ps1 b/.run/src/bin/clippy-fix.ps1 index 1d24f92..1d24f92 100644 --- a/dev_tools/scripts/clippy-fix.ps1 +++ b/.run/src/bin/clippy-fix.ps1 diff --git a/dev_tools/scripts/clippy-fix.sh b/.run/src/bin/clippy-fix.sh index 9771ad4..9771ad4 100755..100644 --- a/dev_tools/scripts/clippy-fix.sh +++ b/.run/src/bin/clippy-fix.sh diff --git a/dev_tools/scripts/clippy.ps1 b/.run/src/bin/clippy.ps1 index 1858873..1858873 100644 --- a/dev_tools/scripts/clippy.ps1 +++ b/.run/src/bin/clippy.ps1 diff --git a/dev_tools/scripts/clippy.sh b/.run/src/bin/clippy.sh index b393545..b393545 100755..100644 --- a/dev_tools/scripts/clippy.sh +++ b/.run/src/bin/clippy.sh diff --git a/.run/src/bin/display-dependency-order.rs b/.run/src/bin/display-dependency-order.rs new file mode 100644 index 0000000..a31c67a --- /dev/null +++ b/.run/src/bin/display-dependency-order.rs @@ -0,0 +1,12 @@ +use tools::{dependency_order::display_dependency_order, eprintln_cargo_style}; + +fn main() { + let order = display_dependency_order(); + if order.is_empty() { + eprintln_cargo_style!("could not find workspace root or mingling crates"); + std::process::exit(1); + } + for path in order { + println!("{}", path.display()); + } +} diff --git a/.run/src/bin/doc.ps1 b/.run/src/bin/doc.ps1 new file mode 100644 index 0000000..fd7afaa --- /dev/null +++ b/.run/src/bin/doc.ps1 @@ -0,0 +1 @@ +cargo doc --workspace --no-deps --features core,macros,builds,structural_renderer,repl,comp,parser,picker,clap,extra_macros --open diff --git a/.run/src/bin/doc.sh b/.run/src/bin/doc.sh new file mode 100644 index 0000000..2d59896 --- /dev/null +++ b/.run/src/bin/doc.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +cargo doc --workspace --no-deps --features core,macros,builds,structural_renderer,repl,comp,parser,picker,clap,extra_macros --open diff --git a/dev_tools/src/bin/docs-code-box-fix.rs b/.run/src/bin/docs-code-box-fix.rs index 21d2cce..21d2cce 100644 --- a/dev_tools/src/bin/docs-code-box-fix.rs +++ b/.run/src/bin/docs-code-box-fix.rs diff --git a/dev_tools/src/bin/docsify-sidebar-gen.rs b/.run/src/bin/docsify-sidebar-gen.rs index f45549a..5beda7f 100644 --- a/dev_tools/src/bin/docsify-sidebar-gen.rs +++ b/.run/src/bin/docsify-sidebar-gen.rs @@ -1,60 +1,97 @@ use std::collections::BTreeMap; use std::fmt::Write; -use std::path::Path; +use std::path::{Path, PathBuf}; 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_cargo_style!("Refresh: _sidebar.md"); - gen_sidebar(); - gen_translation_sidebars(); + gen_all_sidebars(); } -/// Generate _sidebar.md for the primary language -fn gen_sidebar() { +/// Find all README.md under docs/, treat each as a site, and generate _sidebar.md for it. +fn gen_all_sidebars() { let repo_root = find_git_repo().unwrap(); - let pages_root = repo_root.join(PAGES_ROOT); + let docs_root = repo_root.join("docs"); + + let readme_paths = find_all_readmes(&docs_root); + + for readme_path in &readme_paths { + let site_root = readme_path.parent().unwrap(); - let lines = build_sidebar_content(&repo_root.join("docs"), &pages_root, SIDEBAR_HEAD); + let content_dir = find_content_dir(site_root); - let sidebar_path = repo_root.join(SIDEBAR_PATH); - std::fs::write(&sidebar_path, lines).unwrap(); - println_cargo_style!("Generated: {}", sidebar_path.display()); + if let Some(content_dir) = content_dir { + let lines = build_sidebar_content(site_root, &content_dir, SIDEBAR_HEAD); + + let sidebar_path = site_root.join("_sidebar.md"); + std::fs::write(&sidebar_path, lines).unwrap(); + println_cargo_style!("Generated: {}", sidebar_path.display()); + } + } } -/// Generate _sidebar.md inside translation directories -fn gen_translation_sidebars() { - let repo_root = find_git_repo().unwrap(); - let docs_root = repo_root.join("docs"); +/// Recursively find all README.md files under a directory. +fn find_all_readmes(dir: &Path) -> Vec<PathBuf> { + let mut results = Vec::new(); + if let Ok(read_dir) = std::fs::read_dir(dir) { + let mut entries: Vec<_> = read_dir.flatten().collect(); + entries.sort_by_key(|e| e.path()); + for entry in entries { + let path = entry.path(); + if path.is_dir() { + results.extend(find_all_readmes(&path)); + } else if path.file_name().is_some_and(|n| n == "README.md") { + results.push(path); + } + } + } + results +} - if let Ok(read_dir) = std::fs::read_dir(&docs_root) { - for entry in read_dir.flatten() { +/// Find the content directory for a site: +/// 1. Prefer `pages/` if it exists (backward compatible) +/// 2. Fall back to the first subdirectory that contains .md files +fn find_content_dir(site_root: &Path) -> Option<PathBuf> { + // Try pages/ first + let pages_dir = site_root.join("pages"); + if pages_dir.exists() && pages_dir.is_dir() { + return Some(pages_dir); + } + + // Fall back to any subdirectory containing .md files + if let Ok(read_dir) = std::fs::read_dir(site_root) { + let mut entries: Vec<_> = read_dir.flatten().collect(); + entries.sort_by_key(|e| e.path()); + for entry in entries { let path = entry.path(); - let file_name = entry.file_name().to_string_lossy().to_string(); - - // Only process entries starting with '_' that are directories - if file_name.starts_with('_') && path.is_dir() { - // Check if this directory has a 'pages' subdirectory - let pages_dir = path.join("pages"); - if !pages_dir.exists() || !pages_dir.is_dir() { - continue; + if path.is_dir() + && has_markdown_files(&path) { + return Some(path); } + } + } - // The _sidebar.md for a translation directory is relative to that directory, - // so strip_prefix should use the translation directory path, removing the _zh_CN/ prefix - let lines = build_sidebar_content(&path, &pages_dir, "- [Welcome!](README)\n"); + None +} - let sidebar_path = path.join("_sidebar.md"); - std::fs::write(&sidebar_path, lines).unwrap(); - println_cargo_style!("Generated: {}", sidebar_path.display()); +/// Check if a directory (recursively) contains any .md files. +fn has_markdown_files(dir: &Path) -> bool { + if let Ok(read_dir) = std::fs::read_dir(dir) { + for entry in read_dir.flatten() { + let path = entry.path(); + if path.is_dir() { + if has_markdown_files(&path) { + return true; + } + } else if path.extension().is_some_and(|ext| ext == "md") { + return true; } } } + false } /// Build sidebar content: scan .md files in `pages_dir` and return a formatted sidebar string @@ -208,7 +245,7 @@ fn find_git_repo() -> Option<std::path::PathBuf> { fn natural_cmp(a: &str, b: &str) -> std::cmp::Ordering { let num_a = extract_leading_number(a); let num_b = extract_leading_number(b); - num_a.cmp(&num_b) + num_a.cmp(&num_b).then_with(|| a.cmp(b)) } /// Extract the leading numeric prefix from a sidebar link path. @@ -216,12 +253,10 @@ fn natural_cmp(a: &str, b: &str) -> std::cmp::Ordering { /// Looks at the filename stem (after the last `/`) for a number before the first `-`. /// Returns `usize::MAX` for entries without a numeric prefix. fn extract_leading_number(link: &str) -> usize { - if let Some(file_stem) = link.rsplit('/').next() { - if let Some(num_end) = file_stem.find('-') { - if let Ok(num) = file_stem[..num_end].parse::<usize>() { + if let Some(file_stem) = link.rsplit('/').next() + && let Some(num_end) = file_stem.find('-') + && let Ok(num) = file_stem[..num_end].parse::<usize>() { return num; } - } - } usize::MAX } diff --git a/dev_tools/scripts/http-page-preview.ps1 b/.run/src/bin/http-page-preview.ps1 index 8cc3579..8cc3579 100644 --- a/dev_tools/scripts/http-page-preview.ps1 +++ b/.run/src/bin/http-page-preview.ps1 diff --git a/dev_tools/scripts/http-page-preview.sh b/.run/src/bin/http-page-preview.sh index bed4b1c..bed4b1c 100755..100644 --- a/dev_tools/scripts/http-page-preview.sh +++ b/.run/src/bin/http-page-preview.sh diff --git a/dev_tools/scripts/install-mling.ps1 b/.run/src/bin/install-mling.ps1 index bebe9ff..bebe9ff 100755..100644 --- a/dev_tools/scripts/install-mling.ps1 +++ b/.run/src/bin/install-mling.ps1 diff --git a/dev_tools/scripts/install-mling.sh b/.run/src/bin/install-mling.sh index 5f2ee7a..5f2ee7a 100755..100644 --- a/dev_tools/scripts/install-mling.sh +++ b/.run/src/bin/install-mling.sh diff --git a/dev_tools/src/bin/refresh-docs.rs b/.run/src/bin/refresh-docs.rs index 82ef906..82ef906 100644 --- a/dev_tools/src/bin/refresh-docs.rs +++ b/.run/src/bin/refresh-docs.rs diff --git a/dev_tools/src/bin/refresh-feature-mod.rs b/.run/src/bin/refresh-feature-mod.rs index 2255dbc..2255dbc 100644 --- a/dev_tools/src/bin/refresh-feature-mod.rs +++ b/.run/src/bin/refresh-feature-mod.rs diff --git a/dev_tools/src/bin/sync-examples.rs b/.run/src/bin/sync-examples.rs index 0923b33..0923b33 100644 --- a/dev_tools/src/bin/sync-examples.rs +++ b/.run/src/bin/sync-examples.rs diff --git a/dev_tools/src/bin/test-all-markdown-code.rs b/.run/src/bin/test-all-markdown-code.rs index 280fca7..280fca7 100644 --- a/dev_tools/src/bin/test-all-markdown-code.rs +++ b/.run/src/bin/test-all-markdown-code.rs diff --git a/dev_tools/scripts/test-all.ps1 b/.run/src/bin/test-all.ps1 index 231698a..231698a 100644 --- a/dev_tools/scripts/test-all.ps1 +++ b/.run/src/bin/test-all.ps1 diff --git a/dev_tools/scripts/test-all.sh b/.run/src/bin/test-all.sh index b387463..b387463 100644 --- a/dev_tools/scripts/test-all.sh +++ b/.run/src/bin/test-all.sh diff --git a/dev_tools/src/bin/test-examples.rs b/.run/src/bin/test-examples.rs index 539459e..539459e 100644 --- a/dev_tools/src/bin/test-examples.rs +++ b/.run/src/bin/test-examples.rs diff --git a/dev_tools/src/bin/update-version.rs b/.run/src/bin/update-version.rs index 475e54b..9d595bb 100644 --- a/dev_tools/src/bin/update-version.rs +++ b/.run/src/bin/update-version.rs @@ -39,7 +39,9 @@ fn main() { let root_cargo_path = "Cargo.toml"; let root_cargo_content = std::fs::read_to_string(root_cargo_path).expect("Failed to read Cargo.toml"); - let cargo_value: toml::Value = root_cargo_content.parse().expect("Failed to parse Cargo.toml"); + let cargo_value: toml::Value = root_cargo_content + .parse() + .expect("Failed to parse Cargo.toml"); let current_ver = cargo_value["workspace"]["package"]["version"] .as_str() @@ -54,11 +56,11 @@ fn main() { println_cargo_style!("Version: {} -> {}", current_ver, new_ver); // Read version-files.toml - let config_path = Path::new("dev_tools").join("version-files.toml"); + let config_path = Path::new(".run").join("version-files.toml"); let config_str = - std::fs::read_to_string(&config_path).expect("Failed to read dev_tools/version-files.toml"); - let config: Config = toml::from_str(&config_str) - .expect("Failed to parse dev_tools/version-files.toml"); + std::fs::read_to_string(&config_path).expect("Failed to read .run/version-files.toml"); + let config: Config = + toml::from_str(&config_str).expect("Failed to parse .run/version-files.toml"); let mut updated_count = 0; let mut skipped_count = 0; @@ -94,5 +96,9 @@ fn main() { updated_count += 1; } - println_cargo_style!("Done: {} file(s) updated, {} file(s) skipped", updated_count, skipped_count); + println_cargo_style!( + "Done: {} file(s) updated, {} file(s) skipped", + updated_count, + skipped_count + ); } diff --git a/dev_tools/scripts/windows-folder-hide.ps1 b/.run/src/bin/windows-folder-hide.ps1 index 0ab2632..0ab2632 100644 --- a/dev_tools/scripts/windows-folder-hide.ps1 +++ b/.run/src/bin/windows-folder-hide.ps1 diff --git a/.run/src/dependency_order.rs b/.run/src/dependency_order.rs new file mode 100644 index 0000000..7235f12 --- /dev/null +++ b/.run/src/dependency_order.rs @@ -0,0 +1,183 @@ +use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; + +/// Parse Cargo.toml content and return dependency names that start with `prefix`. +fn parse_mingling_deps(content: &str, prefix: &str) -> Vec<String> { + let value: toml::Value = match content.parse() { + Ok(v) => v, + Err(_) => return Vec::new(), + }; + + let mut names = Vec::new(); + + // Check [dependencies] + if let Some(deps) = value.get("dependencies").and_then(|d| d.as_table()) { + for key in deps.keys() { + if key.starts_with(prefix) { + names.push(key.clone()); + } + } + } + + // Check [build-dependencies] + if let Some(deps) = value.get("build-dependencies").and_then(|d| d.as_table()) { + for key in deps.keys() { + if key.starts_with(prefix) { + names.push(key.clone()); + } + } + } + + names +} + +/// Read workspace members from the root Cargo.toml. +fn get_workspace_members(workspace_root: &std::path::Path) -> Vec<String> { + let cargo_path = workspace_root.join("Cargo.toml"); + let content = match std::fs::read_to_string(&cargo_path) { + Ok(c) => c, + Err(_) => return Vec::new(), + }; + + let value: toml::Value = match content.parse() { + Ok(v) => v, + Err(_) => return Vec::new(), + }; + + value + .get("workspace") + .and_then(|w| w.get("members")) + .and_then(|m| m.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.as_str().map(String::from)) + .collect() + }) + .unwrap_or_default() +} + +/// Hierarchical topological sort (process layer by layer, sort siblings alphabetically). +/// +/// `dep_map` maps each crate to the list of crates it depends on. +/// Returns the dependency order (dependent crates come first, dependents come later), +/// with crates at the same layer (which can be built in parallel) sorted alphabetically. +fn topological_sort( + all_crates: &HashSet<String>, + dep_map: &HashMap<String, Vec<String>>, +) -> Vec<String> { + // in_degree[crate] = number of remaining mingling_* dependencies not yet processed + let mut in_degree: HashMap<&str, usize> = HashMap::new(); + // reverse[dependency] = list of crates that depend on it + let mut reverse: HashMap<&str, Vec<&str>> = HashMap::new(); + + for name in all_crates { + in_degree.entry(name.as_str()).or_insert(0); + reverse.entry(name.as_str()).or_default(); + } + + for (crate_name, deps) in dep_map { + for dep in deps { + if all_crates.contains(dep.as_str()) { + reverse + .get_mut(dep.as_str()) + .unwrap() + .push(crate_name.as_str()); + *in_degree.get_mut(crate_name.as_str()).unwrap() += 1; + } + } + } + + let mut result: Vec<String> = Vec::new(); + + // Process layer by layer: all crates with in_degree == 0 in one batch form a layer + loop { + let mut current: Vec<&str> = all_crates + .iter() + .filter(|n| in_degree.get(n.as_str()).copied().unwrap_or(0) == 0) + .filter(|n| !result.iter().any(|r| r.as_str() == n.as_str())) + .map(|s| s.as_str()) + .collect(); + + if current.is_empty() { + break; + } + + current.sort(); + result.extend(current.iter().map(|s| s.to_string())); + + for &node in ¤t { + if let Some(dependents) = reverse.get(node) { + for &dependent in dependents { + if let Some(degree) = in_degree.get_mut(dependent) { + *degree -= 1; + } + } + } + } + } + + result +} + +/// Find the workspace root by looking for a Cargo.toml with `[workspace]` members. +/// Starts from `start` and walks up the directory tree. +pub fn find_workspace_root(start: &std::path::Path) -> Option<PathBuf> { + let mut current = Some(std::fs::canonicalize(start).unwrap_or_else(|_| start.to_path_buf())); + while let Some(dir) = current { + let members = get_workspace_members(&dir); + if !members.is_empty() { + return Some(dir); + } + current = dir.parent().map(|p| p.to_path_buf()); + } + None +} + +/// Output all crate paths in dependency order +#[allow(unused)] +pub fn display_dependency_order() -> Vec<PathBuf> { + let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + + let workspace_root = match find_workspace_root(&cwd) { + Some(root) => root, + None => return Vec::new(), + }; + + // Read workspace members from root Cargo.toml + let members = get_workspace_members(&workspace_root); + + // Filter to crates starting with "mingling" + let mingling_crates: HashSet<String> = members + .into_iter() + .filter(|m| m.starts_with("mingling")) + .collect(); + + if mingling_crates.is_empty() { + return Vec::new(); + } + + // Build dependency graph + let mut dep_map: HashMap<String, Vec<String>> = HashMap::new(); + + for crate_name in &mingling_crates { + let cargo_path = workspace_root.join(crate_name).join("Cargo.toml"); + let content = match std::fs::read_to_string(&cargo_path) { + Ok(c) => c, + Err(_) => { + dep_map.insert(crate_name.clone(), Vec::new()); + continue; + } + }; + let deps = parse_mingling_deps(&content, "mingling"); + // Only keep deps that are actually in our set + let filtered: Vec<String> = deps + .into_iter() + .filter(|d| mingling_crates.contains(d.as_str())) + .collect(); + dep_map.insert(crate_name.clone(), filtered); + } + + let sorted = topological_sort(&mingling_crates, &dep_map); + + sorted.into_iter().map(PathBuf::from).collect() +} diff --git a/dev_tools/src/lib.rs b/.run/src/lib.rs index d38a156..d52c7fd 100644 --- a/dev_tools/src/lib.rs +++ b/.run/src/lib.rs @@ -1,3 +1,4 @@ +pub mod dependency_order; pub mod verify; use colored::Colorize; @@ -124,7 +125,7 @@ pub fn wprintln_cargo_style(str: impl Into<String>) { ); } -/// Run a shell command and return its exit status. +/// Run a shell command in the current directory and return its exit status. /// /// # Panics /// @@ -134,6 +135,20 @@ pub fn wprintln_cargo_style(str: impl Into<String>) { /// /// Returns `Err` with the exit code if the command finishes with a non-zero exit code. pub fn run_cmd(cmd: impl Into<String>) -> Result<(), i32> { + let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); + run_cmd_with_dir(cmd.into(), &cwd) +} + +/// Run a shell command in the specified directory and return its exit status. +/// +/// # Panics +/// +/// Panics if the shell command cannot be spawned (e.g. the shell binary is not found). +/// +/// # Errors +/// +/// Returns `Err` with the exit code if the command finishes with a non-zero exit code. +pub fn run_cmd_with_dir(cmd: impl Into<String>, dir: &std::path::Path) -> Result<(), i32> { let shell = if cfg!(target_os = "windows") { "powershell" } else { @@ -142,7 +157,7 @@ pub fn run_cmd(cmd: impl Into<String>) -> Result<(), i32> { 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("."))) + .current_dir(dir) .status() .expect("failed to execute command"); @@ -159,6 +174,18 @@ pub fn run_cmd(cmd: impl Into<String>) -> Result<(), i32> { /// On success returns `Ok(combined_output)`. On failure returns `Err((exit_code, stderr))`. /// Stderr falls back to stdout if stderr is empty. pub fn run_cmd_capture(cmd: impl Into<String>) -> Result<String, (i32, String)> { + let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); + run_cmd_capture_with_dir(cmd.into(), &cwd) +} + +/// Run a shell command in the specified directory and capture its combined stdout+stderr output. +/// +/// On success returns `Ok(combined_output)`. On failure returns `Err((exit_code, stderr))`. +/// Stderr falls back to stdout if stderr is empty. +pub fn run_cmd_capture_with_dir( + cmd: impl Into<String>, + dir: &std::path::Path, +) -> Result<String, (i32, String)> { let shell = if cfg!(target_os = "windows") { "powershell" } else { @@ -167,7 +194,7 @@ pub fn run_cmd_capture(cmd: impl Into<String>) -> Result<String, (i32, String)> let output = std::process::Command::new(shell) .arg("-c") .arg(cmd.into()) - .current_dir(std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))) + .current_dir(dir) .output() .expect("failed to execute command"); @@ -316,8 +343,8 @@ pub fn cargo_tomls() -> Vec<std::path::PathBuf> { for entry in entries.flatten() { let path = entry.path(); if path.is_dir() { - // Skip the dev_tools directory - if path.file_name().and_then(|n| n.to_str()) == Some("dev_tools") { + // Skip the .run directory + if path.file_name().and_then(|n| n.to_str()) == Some(".run") { continue; } dirs.push(path); diff --git a/dev_tools/src/verify.rs b/.run/src/verify.rs index 2ddff0c..0a4b354 100644 --- a/dev_tools/src/verify.rs +++ b/.run/src/verify.rs @@ -76,10 +76,10 @@ fn parse_single_block(lines: &[&str], start: usize, source_file: &str) -> Option // @@@ lines: strip the prefix and treat as regular Rust code // These lines are hidden in the rendered docs (filtered by a docsify plugin) // but must still compile. - if trimmed.starts_with("@@@") { + if let Some(stripped) = trimmed.strip_prefix("@@@") { in_header = false; // Strip @@@ and optionally one following space - let code = trimmed[3..].trim_start(); + let code = stripped.trim_start(); if code.contains("fn main") { has_main = true; } diff --git a/dev_tools/version-files.toml b/.run/version-files.toml index ca104d2..42d9b4d 100644 --- a/dev_tools/version-files.toml +++ b/.run/version-files.toml @@ -17,3 +17,15 @@ pattern = "version = \"{VER}\"" [[file]] file = "./docs/res/guide.txt" pattern = "mingling = \"{VER}\"" + +[[file]] +file = "./mingling_picker/README.md" +pattern = "version = \"{VER}\"" + +[[file]] +file = "./mingling_picker/README.md" +pattern = "mingling_picker = \"{VER}\"" + +[[file]] +file = "./mingling_picker_macros/README.md" +pattern = "version = \"{VER}\"" diff --git a/.vscode/settings.json b/.vscode/settings.json index 9dc29f4..7472e1c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,11 @@ { "rust-analyzer.check.command": "clippy", "rust-analyzer.checkOnSave": true, + "rust-analyzer.linkedProjects": [ + ".run/Cargo.toml", + "mingling_picker/Cargo.toml", + "mingling_pathf/test/Cargo.toml", + "mingling_picker/test/Cargo.toml", + ], + "rust-analyzer.cargo.features": ["mingling_support"], } diff --git a/.zed/settings.json b/.zed/settings.json index 067be5a..12c901e 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -4,6 +4,15 @@ "initialization_options": { "checkOnSave": true, "check": { "command": "clippy" }, + "linkedProjects": [ + ".run/Cargo.toml", + "mingling_picker/Cargo.toml", + "mingling_pathf/test/Cargo.toml", + "mingling_picker/test/Cargo.toml", + ], + "cargo": { + "features": ["mingling_support"], + }, }, }, }, diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ec479d..361338f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,17 @@ This file tracks all notable changes to the Mingling project. Each release entry The format follows a human-readable changelog convention, with sections organized by release version and change type. -Any contributor making changes to the project must record their changes in this file under the appropriate release section, using the established format and change type categories *(Features, Fixes, Optimizations, Tests, BREAKING CHANGES, etc.)*. +Any contributor making changes to the project must record their changes in this file under the appropriate release section, using the established format and change type categories _(Features, Fixes, Optimizations, Tests, BREAKING CHANGES, etc.)_. ## TOC **- Milestone.1 "MVP" -** -- [Release 0.2.0 (Unreleased)](#release-020-unreleased) +- [Unreleased](#unreleased) +- [Release 0.3.0 (Unreleased)](#release-030-unreleased) +- [Release 0.2.2 (2026-07-10)](#release-022-2026-07-10) +- [Release 0.2.1 (2026-07-01)](#release-021-2026-07-01) +- [Release 0.2.0 (2026-06-30)](#release-020-2026-06-30) - [Release 0.1.9 (2026-05-29)](#release-019-2026-05-29) - [Release 0.1.8 (2026-05-18)](#release-018-2026-05-18) - [Release 0.1.7 (2026-05-04)](#release-017-2026-05-04) @@ -26,7 +30,117 @@ Any contributor making changes to the project must record their changes in this ## Contents -### Release 0.2.0 (Unreleased) +### Unreleased + +#### Fixes: + +None + +#### Optimizations: + +None + +#### Features: + +None + +#### **BREAKING CHANGES** (API CHANGES): + +None + +--- + +### Release 0.3.0 (Unreleased) + +#### Fixes: + +None + +#### Optimizations: + +None + +#### Features: + +1. **[`core`]** Added `RenderResult::new()` method for creating a new `RenderResult` with default values (empty text and exit code 0). This provides a more explicit and discoverable constructor compared to `RenderResult::default()`, making it clearer when a fresh result is being created for use with `write!`/`writeln!`. + + ```rust + let mut result = RenderResult::new(); + writeln!(result, "Hello!").ok(); + result + ``` + + The method is equivalent to `RenderResult::default()` but serves as a more idiomatic entry point for renderer functions. + +#### **BREAKING CHANGES** (API CHANGES): + +1. **[`macros:renderer`]** **[`macros:help`]** Removed `r_println!` and `r_print!` macros. The `#[renderer]` and `#[help]` macros no longer implicitly inject an internal `RenderResult` variable or provide `r_println!` / `r_print!` macros. + + Renderers and help functions must now explicitly create and return a `RenderResult`: + + ```rust + use mingling::prelude::*; + + #[renderer] + fn render_greeting(greeting: ResultGreeting) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Hello, {}!", *greeting).ok(); + result + } + ``` + + All examples, docs, and test cases across the repository have been updated to use the new pattern: creating a `RenderResult` with `RenderResult::new()` or `RenderResult::default()`, writing with `write!`/`writeln!` from `std::io::Write`, and returning the result. + +--- + +## Release 0.2.2 (2026-07-10) + +### Unreleased + +#### Fixes: + +1. **[`macros:structural_data`]** Fixed `group_structural!` macro to correctly generate `use` statements for multi-segment type paths (e.g., `crate::MyType`). Previously, when an aliased type had only one segment, the macro used the alias name (`type_name`) instead of the original type's last segment for `super::` imports, which could cause compilation errors. Now correctly extracts the last segment from the original `type_path` for single-segment fallback imports. + +#### Optimizations: + +None + +#### Features: + +None + +#### **BREAKING CHANGES** (API CHANGES): + +None + +--- + +### Release 0.2.1 (2026-07-01) + +#### Fixes: + +1. **[`macros`]** Fixed false positives in `entry_has_variant` caused by bare substring matching in the third `contains` check. + When a longer variant (e.g., `EntryListAlias`) is registered first, followed by a shorter variant that shares the same prefix (e.g., `EntryList`), + `"=> EntryList"` would incorrectly match as a substring of `"=> EntryListAlias,"`, causing a false duplicate registration detection. + Now changed to use `find` + trailing character boundary validation, ensuring the character immediately after the match is not an identifier character (letter/digit/underscore). + + Affected scope: Deduplication logic for `#[chain]`, `#[renderer]`, `#[help]`, and `#[completion]` registration. + +#### Optimizations: + +None + +#### Features: + +None + +#### **BREAKING CHANGES** (API CHANGES): + +None + +--- + +### Release 0.2.0 (2026-06-30) > [!IMPORTANT] > Starting from 0.2.0, Mingling's GitHub repository has been migrated from [catilgrass/mingling](https://github.com/catilgrass/mingling) to [mingling-rs/mingling](https://github.com/mingling-rs/mingling). @@ -461,10 +575,11 @@ let value = route!(prev.pick_or_route((), Error::default()).unpack()); 7. **[`core`]** **[`structural_renderer`]** Added the `pack_err_structural!`, `pack_structural!`, and `group_structural!` macros for creating types that support structured output (JSON/YAML/TOML/RON). These are like `pack_err!`, `pack!`, and `group!` respectively, but also mark the type with the `StructuralData` trait, enabling the `StructuralRenderer` to serialize them. 8. **[`core`]** **[`structural_renderer`]** Added the `StructuralData` derive macro and sealed trait, decoupling structured output from `Groupped`. Previously, under the `structural_renderer` feature, all `pack!` and `pack_err!` types automatically derived `Serialize`. Now, structured output is an opt-in property controlled by `StructuralData`: - - `pack!` / `pack_err!` / `group!` no longer derive `Serialize` even when `structural_renderer` is enabled. - - To enable structured output, use `pack_structural!` / `pack_err_structural!` / `group_structural!` or the `#[derive(StructuralData)]` marker. - - The `Groupped` trait no longer requires `Serialize` bounds, and `AnyOutput::new` no longer requires `Serialize`. - - `StructuralRenderer::render` now accepts `T: StructuralData + Send` instead of `T: Serialize + Send`, and the individual format methods (`render_to_json`, etc.) are now private. + +- `pack!` / `pack_err!` / `group!` no longer derive `Serialize` even when `structural_renderer` is enabled. +- To enable structured output, use `pack_structural!` / `pack_err_structural!` / `group_structural!` or the `#[derive(StructuralData)]` marker. +- The `Groupped` trait no longer requires `Serialize` bounds, and `AnyOutput::new` no longer requires `Serialize`. +- `StructuralRenderer::render` now accepts `T: StructuralData + Send` instead of `T: Serialize + Send`, and the individual format methods (`render_to_json`, etc.) are now private. 9. **[`core`]** **[`structural_renderer`]** Added `mingling::__private::StructuralDataSealed` and `mingling::__private::StructuralData` (re-exported from `mingling_core::renderer::structural::structural_data`) to support the sealed trait pattern. The `StructuralData` trait is only implementable via the derive macro or the `_structural` macro variants. @@ -609,12 +724,12 @@ program.with_setup(DirectoryEnvironmentSetup::<ThisProgram>::default()); 11. **[`mingling`]** Added four new resource types for directory environments: - - `ResCurrentDir` — Wraps `std::env::current_dir()` as a global resource. Provides `new() -> Result`, `Default` (panics on failure), and conversions from/to `PathBuf`, `&Path`, and `&PathBuf`. - - `ResCurrentExe` — Wraps `std::env::current_exe()` as a global resource. Provides `new() -> Result`, `Default` (panics on failure), and conversions from/to `PathBuf`, `&Path`, and `&PathBuf`. - - `ResHomeDir` — Wraps the user's home directory (`$HOME` on Unix, `%USERPROFILE%` on Windows) as a global resource. Provides `new() -> Result`, `Default` (panics on failure), and conversions from/to `PathBuf`, `&Path`, and `&PathBuf`. - - `ResTempDir` — Wraps `std::env::temp_dir()` as a global resource. Provides `new()` (infallible), `Default`, and conversions from/to `PathBuf`, `&Path`, and `&PathBuf`. +- `ResCurrentDir` — Wraps `std::env::current_dir()` as a global resource. Provides `new() -> Result`, `Default` (panics on failure), and conversions from/to `PathBuf`, `&Path`, and `&PathBuf`. +- `ResCurrentExe` — Wraps `std::env::current_exe()` as a global resource. Provides `new() -> Result`, `Default` (panics on failure), and conversions from/to `PathBuf`, `&Path`, and `&PathBuf`. +- `ResHomeDir` — Wraps the user's home directory (`$HOME` on Unix, `%USERPROFILE%` on Windows) as a global resource. Provides `new() -> Result`, `Default` (panics on failure), and conversions from/to `PathBuf`, `&Path`, and `&PathBuf`. +- `ResTempDir` — Wraps `std::env::temp_dir()` as a global resource. Provides `new()` (infallible), `Default`, and conversions from/to `PathBuf`, `&Path`, and `&PathBuf`. - All four types implement `Deref<Target = PathBuf>`, `DerefMut`, `AsRef<Path>`, `Clone`, `Debug`, and `PartialEq`. +All four types implement `Deref<Target = PathBuf>`, `DerefMut`, `AsRef<Path>`, `Clone`, `Debug`, and `PartialEq`. #### **BREAKING CHANGES** (API CHANGES): @@ -1006,7 +1121,7 @@ let (name, age) = Picker::new(prev.inner) --- -### Yanked 0.1.6 (2026-04-24) +### Yanked 0.1.6 (2026-04-24) > [!CAUTION] > @@ -1018,7 +1133,7 @@ let (name, age) = Picker::new(prev.inner) --- -### Release 0.1.6 (2026-04-20) +### Release 0.1.6 (2026-04-20) `Mingling` 0.1.6 primarily focuses on optimizing the writing experience and code completion. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bf66adc..b10e96d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,30 +4,115 @@ First of all, thank you for your interest in Mingling! 🎉 Whether it's fixing Before contributing, we recommend reading [README](README.md) to get an overview of the project. +## 1. Project Structure 📦 + +| Category | Path/Name | Description | +| --------------------------- | ------------------------- | ------------------------------------------------------------------ | +| **Entry crate** | `mingling/` | Project entry point | +| **Core library** | `mingling_core/` | Imported as an external dependency | +| **Macro library** | `mingling_macros/` | Imported as an external dependency | +| **Mingling Pathfinder** | `mingling_pathf/` | Build-time module path resolution for types | +| **Mingling Picker2** | `mingling_picker/` | Mingling Arguments Parser | +| **Mingling Picker2 Macros** | `mingling_picker_macros/` | Mingling Arguments Parser Macros | +| **Scaffolding tool** | `mling/` | Scaffolding tool `mingling-cli` | +| **Examples** | `examples/` | To add expected output tests, modify `examples/test-examples.toml` | +| **Documents** | `docs/` | All documents | +| **Dev Documents** | `docs/dev/` | Internal documents | +| **Resources** | `docs/res/` | All resources | +| **Development tools** | `.run/src/bin` | Contains scripts and Rust tools | +| **CI** | `.run/src/bin/ci.rs` | Can be invoked directly via `cargo ci` | +| **Temporary files** | `.temp/` | Ignored by `.gitignore` | + +## 2. How to Contribute + +### Code Contribution + +If you'd like to contribute to `mingling`, `mingling_core`, `mingling_macros`, or `mingling_pathf`, first share your idea on the [Github Issue](https://github.com/mingling-rs/mingling/issues) page to confirm before starting work. + +- **Before making changes**, make sure your branch stays **as close as possible** to the upstream `main` branch. +- **After finishing**, run `cargo ci` locally (see [ABOUT CI](https://mingling-rs.github.io/mingling/docs/dev/#/pages/abouts/ci) for how it works). If `cargo ci` passes locally, your changes are most likely correct. + +### Example Code Contribution + +To add or modify examples under `examples/`, follow these rules: + +- Place each example in `examples/<example-dir>/` +- Each dir must contain a `page.toml` file describing the example's metadata +- `page.toml` format: + +```toml +[example] +id = "example-id" # Unique identifier +name = "Example Name" # Display name (optional, defaults to dir name) +icon = "📦" # Icon (optional, defaults to "📦") +category = "" # Category (optional) +desc = "Description" # Description (optional) +tags = ["tag1", "tag2"] # Tags (optional) +files = ["Cargo.toml", "src/main.rs"] +``` + +If you change expected behavior, update the test assertions in `examples/test-examples.toml`. +After editing examples, run these scripts to keep things in sync: -## 1. Project Structure +```bash +# Ensure code compiles +./run.sh build-all -| Category | Path/Name | Description | -|----------|-----------|-------------| -| **Entry crate** | `mingling/` | Project entry point | -| **Core library** | `mingling_core/` | Imported as an external dependency | -| **Macro library** | `mingling_macros/` | Imported as an external dependency | -| **Examples** | `examples/` | To add expected output tests, modify `examples/test-examples.toml` | -| **Documentation, resources** | `docs/` | All documentation and resource files | -| **Development tools** | `dev_tools/` | Contains scripts and Rust tools | -| Scripts | `dev_tools/scripts/` | Helper `.sh`/`.ps1`/`.py` scripts, executed via `./run-tools.sh` or `.\run-tools.ps1` | -| Rust tools | `dev_tools/src/bin/` | Same as above | -| CI check entry | `dev_tools/src/bin/ci.rs` | Can be invoked directly via `cargo ci` | -| **Scaffolding tool** | `mling/` | Scaffolding tool `mingling-cli` | -| **Temporary files** | `.temp/` | Ignored by `.gitignore` | +# Ensure code style +./run.sh clippy +# Sync page.toml info to docs/example-pages/examples.json +./run.sh sync-examples +# Check all examples behave as expected +./run.sh test-examples -## 2. Submission Guide +# Sync examples content into mingling/src/example_docs.rs +./run.sh refresh-docs -1. **Pull Request** +# (Optional) Preview the Example Viewer in a browser +# Requires: Python +./run.sh http-page-preview +# http://127.0.0.1:3000/ +``` + +### Documentation Contribution + +To contribute docs, edit files under `docs/`. For other language translations, refer to the structure under `docs/zh_CN`. + +- **When editing docs**, prioritize **Chinese docs** first, then **English docs** — since I ([@Weicao-CatilGrass](https://github.com/Weicao-CatilGrass)) am a native Chinese speaker, this is more efficient. +- If your changes involve code, check the [Code Verify System](https://mingling-rs.github.io/mingling/docs/dev/#/pages/abouts/code-verify-system), which explains how CI checks code. +- **Before submitting**, always run: + +```bash +# Fix code block issues in docsify +./run.sh docs-code-box-fix + +# Generate sidebar +./run.sh docsify-sidebar-gen + +# Verify all Markdown code blocks compile +./run.sh test-all-markdown-code +``` + +### Web Frontend Contribution +No strict requirements here — just modify the relevant `*.html` files. Preview with: + +```bash +# Requires: Python +./run.sh http-page-preview +# http://127.0.0.1:3000/ +``` + +### Dev Tool Contribution + +`Mingling CI` code is under strict review. If you want to improve `mingling`'s CI pipeline or other dev tools (under `.run/`), **please** first file an [Issue](https://github.com/mingling-rs/mingling/issues) and contact [Weicao-CatilGrass](https://github.com/Weicao-CatilGrass)! + +## 3. Submission Guide 🖊 + +1. **Pull Request** - Submit a GitHub Pull Request and @Reviewer **[Weicao-CatilGrass](https://github.com/Weicao-CatilGrass)** for review - Or send patches to **catil_grass@qq.com** @@ -51,9 +136,7 @@ Before contributing, we recommend reading [README](README.md) to get an overview 6. **Binary Resources** - For binary resource files (images, etc.), please be cautious about adding them to avoid repository bloat - - -## 3. Documentation Contribution +## 4. Documentation Contribution 📕 ### Documentation Location @@ -66,26 +149,25 @@ After editing documentation, refresh relevant files: ```bash # Refresh sidebar and README sync -./run-tools.sh docsify-sidebar-gen -./run-tools.sh refresh-docs +./run.sh docsify-sidebar-gen +./run.sh refresh-docs # Fix code block blank line issues -./run-tools.sh docs-code-box-fix +./run.sh docs-code-box-fix ``` These steps are included in `cargo ci`; running `cargo ci` will execute them automatically. +> [!TIP] +> You can check the [ABOUT CI](https://mingling-rs.github.io/mingling/docs/dev/#/pages/abouts/ci) section to learn how "Mingling CI" works. - -## 4. Regarding AI Agent Usage +## 5. Regarding AI Agent Usage 🤖 - You are free to use AI agents to assist development — no restrictions - **Humans are the final decision-makers**, everything is subject to human judgment - Please **DO NOT** leave AI instruction files like `CLAUDE.md` in the repository root. Mingling currently has no plans to introduce **Harness Engineering** - - -## 5. License +## 6. License 📖 Mingling uses the **MIT + Apache 2.0** dual license. For details, please see: @@ -343,12 +343,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" [[package]] +name = "just_fmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" + +[[package]] name = "just_template" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb99a3c1dee7299c57b26ef927f15535da0fbc93d6deb1d1114cae1337be4fb" dependencies = [ - "just_fmt", + "just_fmt 0.1.2", "just_template_macros", ] @@ -401,11 +407,12 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling", "mingling_core", "mingling_macros", + "mingling_picker", "serde", "size", "tokio", @@ -413,12 +420,12 @@ dependencies = [ [[package]] name = "mingling-cli" -version = "0.2.0" +version = "0.3.0" dependencies = [ "chrono", "colored", "dirs", - "just_fmt", + "just_fmt 0.1.2", "mingling", "serde", "serde_json", @@ -427,10 +434,10 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "env_logger", - "just_fmt", + "just_fmt 0.2.0", "just_template", "log", "mingling_pathf", @@ -443,9 +450,9 @@ dependencies = [ [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", "proc-macro2", "quote", "syn", @@ -453,10 +460,28 @@ dependencies = [ [[package]] name = "mingling_pathf" -version = "0.2.0" +version = "0.3.0" +dependencies = [ + "just_fmt 0.2.0", + "proc-macro2", + "syn", +] + +[[package]] +name = "mingling_picker" +version = "0.3.0" +dependencies = [ + "just_fmt 0.2.0", + "mingling_core", + "mingling_picker_macros", +] + +[[package]] +name = "mingling_picker_macros" +version = "0.3.0" dependencies = [ - "just_fmt", "proc-macro2", + "quote", "syn", ] @@ -1,6 +1,22 @@ [workspace] resolver = "2" -members = ["mingling", "mingling_core", "mingling_macros", "mingling_pathf", "mling"] +members = [ + # Core + "mingling", + "mingling_core", + "mingling_macros", + + # Pathfinder + "mingling_pathf", + + # Picker2 + "mingling_picker", + "mingling_picker_macros", + + # Scaffolding tool + "mling" +] + exclude = [ # README-Tests "./temp/*", @@ -9,7 +25,7 @@ exclude = [ "examples/*", # Dev Tools - "dev_tools", + ".run", # Tests "mingling_core/tests/*", @@ -20,8 +36,10 @@ exclude = [ mingling_core = { path = "mingling_core", default-features = false } mingling_macros = { path = "mingling_macros", default-features = false } mingling_pathf = { path = "mingling_pathf", default-features = false } +mingling_picker = { path = "mingling_picker", default-features = false } +mingling_picker_macros = { path = "mingling_picker_macros", default-features = false } -just_fmt = "0.1.2" +just_fmt = "0.2.0" just_template = "0.2.0" serde = { version = "1.0.228", features = ["derive"] } @@ -43,7 +61,7 @@ log = "0.4.33" env_logger = "0.11.11" [workspace.package] -version = "0.2.0" +version = "0.3.0" edition = "2024" license = "MIT OR Apache-2.0" repository = "https://github.com/mingling-rs/mingling" @@ -18,6 +18,7 @@ <img src="https://img.shields.io/crates/size/mingling"> <img src="https://img.shields.io/crates/v/mingling?style=flat"> <img src="https://img.shields.io/docsrs/mingling?style=flat"> + <img src="https://img.shields.io/github/actions/workflow/status/mingling-rs/mingling/ci.yml"> </p> > [!WARNING] @@ -53,7 +54,8 @@ - 📖 [Mainpage](https://mingling-rs.github.io/mingling/) - 📖 [Examples](https://mingling-rs.github.io/mingling/docs/examples.html) - 📖 [docs.rs](https://docs.rs/mingling/latest/mingling/) -- 📖 Helpdoc [EN](https://mingling-rs.github.io/mingling/docs/doc.html#/) [中文](https://mingling-rs.github.io/mingling/docs/_zh_CN/index.html#/) +- 📖 [Helpdoc](https://mingling-rs.github.io/mingling/docs/doc.html#/) Or [帮助文档](https://mingling-rs.github.io/mingling/docs/_zh_CN/index.html#/) +- 🔍 [Devdoc](https://mingling-rs.github.io/mingling/docs/dev/) <h1 align="center"> Getting Started @@ -63,7 +65,7 @@ Add Mingling to your `Cargo.toml`: ```toml [dependencies.mingling] -version = "0.2.0" +version = "0.3.0" features = [] ``` @@ -179,27 +181,36 @@ Key points: ### 3. The Renderer — "#[renderer]" — How Output Works -The `#[renderer]` attribute turns a function into an output handler. It receives the final result of a chain and writes it to the terminal. +The `#[renderer]` attribute turns a function into an output handler. It receives the final result of a chain and returns a `RenderResult`. ```rust +use mingling::macros::pack; +use mingling::prelude::*; +use std::io::Write; + pack!(ResultGreeting = String); #[renderer] -fn render_greeting(greeting: ResultGreeting) { - r_println!("Hello, {}!", *greeting); +fn render_greeting(greeting: ResultGreeting) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Hello, {}!", *greeting).ok(); + result } ``` -Inside a renderer, use `r_print!` / `r_println!` to write to the output buffer. This is not `println!` — it writes into Mingling's internal `RenderResult` buffer, which is flushed at the end of the pipeline. +Inside a renderer, create a `RenderResult`, write to it using `write!` / `writeln!` (from [`std::io::Write`](https://doc.rust-lang.org/std/io/trait.Write.html)), and return it. The output is captured in the buffer and flushed by the framework at the end of the pipeline. You can write renderers for **any type** in your program, including error types: ```rust use mingling::prelude::*; +use std::io::Write; #[renderer] -fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) { - r_println!("Command not found: [{}]", err.join(" ")); +fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Command not found: [{}]", err.join(" ")).ok(); + result } ``` @@ -281,14 +292,17 @@ Help is just another attribute macro. When the user passes `--help` or `-h`, the Enable it by adding `BasicProgramSetup`: ```rust -use mingling::{macros::help, setup::BasicProgramSetup}; +use mingling::{macros::help, prelude::*, setup::BasicProgramSetup}; +use std::io::Write; dispatcher!("greet", CMDGreet => EntryGreet); #[help] -fn help_greet(_prev: EntryGreet) { - r_println!("Usage: greet <NAME>"); - r_println!("Greets the user with the given name."); +fn help_greet(_prev: EntryGreet) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Usage: greet <NAME>").ok(); + writeln!(result, "Greets the user with the given name.").ok(); + result } fn main() { @@ -396,6 +410,10 @@ fn complete_lang(_: &ShellContext) -> Suggest { Mingling doesn't use `?` operator propagation. Instead, errors are just **alternative results** that flow through the same chain/render pipeline. Create error types with `pack!` and route to them with `.to_render()`: ```rust +use mingling::macros::pack; +use mingling::prelude::*; +use std::io::Write; + dispatcher!("hello", CMDHello => EntryHello); pack!(ResultName = String); pack!(ErrorNoNameProvided = ()); @@ -415,13 +433,17 @@ fn handle(args: EntryHello) -> Next { } #[renderer] -fn render_no_name(_: ErrorNoNameProvided) { - r_println!("No name provided"); +fn render_no_name(_: ErrorNoNameProvided) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "No name provided").ok(); + result } #[renderer] -fn render_too_long(len: ErrorNameTooLong) { - r_println!("Name too long: {} > 10", *len); +fn render_too_long(len: ErrorNameTooLong) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Name too long: {} > 10", *len).ok(); + result } ``` @@ -478,6 +500,8 @@ fn change_dir(prev: EntryCd, current_dir: &mut ResCurrentDir) -> Next { Resources can also be injected into `#[renderer]`: ```rust +use mingling::prelude::*; +use std::io::Write; dispatcher!("current", CMDCurrent => EntryCurrent); @@ -487,8 +511,10 @@ struct ResCurrentDir { } #[renderer] -fn render_current(_: EntryCurrent, current_dir: &ResCurrentDir) { - r_println!("Current directory: {}", current_dir.current_dir.display()); +fn render_current(_: EntryCurrent, current_dir: &ResCurrentDir) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Current directory: {}", current_dir.current_dir.display()).ok(); + result } ``` @@ -534,7 +560,8 @@ If you prefer clap's powerful argument parsing, use `#[dispatcher_clap]`. It gen // clap = "4" use mingling::macros::dispatcher_clap; -use mingling::Groupped; +use mingling::prelude::*; +use std::io::Write; #[derive(Default, clap::Parser, Groupped)] #[dispatcher_clap( @@ -551,15 +578,19 @@ pub struct EntryGreet { } #[renderer] -fn render_greet(greet: EntryGreet) { - r_print!("Hello, "); - for _ in 0..greet.repeat { r_print!("{}", greet.name); } - r_println!("!"); +fn render_greet(greet: EntryGreet) -> RenderResult { + let mut result = RenderResult::new(); + write!(result, "Hello, ").ok(); + for _ in 0..greet.repeat { write!(result, "{}", greet.name).ok(); } + writeln!(result, "!").ok(); + result } #[renderer] -fn render_parse_error(err: ErrorGreetParsed) { - r_println!("{}", *err); +fn render_parse_error(err: ErrorGreetParsed) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "{}", *err).ok(); + result } ``` @@ -685,6 +716,7 @@ use mingling::{prelude::*, setup::StructuralRendererSetup}; use mingling::Groupped; use mingling::StructuralData; use serde::Serialize; +use std::io::Write; dispatcher!("render", CMDRender => EntryRender); @@ -701,8 +733,10 @@ fn render_info(args: EntryRender) -> Next { } #[renderer] -fn render_info_result(info: ResultInfo) { - r_println!("{} is {} years old", info.name, info.age); +fn render_info_result(info: ResultInfo) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "{} is {} years old", info.name, info.age).ok(); + result } fn main() { @@ -738,6 +772,7 @@ Enable the `async` feature to use `async fn` inside `#[chain]`: // Dependencies: // tokio = { version = "1", features = ["full"] } +use std::io::Write; use std::time::Duration; dispatcher!("download", CMDDownload => EntryDownload); @@ -755,8 +790,10 @@ async fn download_file(name: String) -> ResultDownloaded { } #[renderer] -fn render_downloaded(result: ResultDownloaded) { - r_println!("\"{}\" downloaded.", *result); +fn render_downloaded(result: ResultDownloaded) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "\"{}\" downloaded.", *result).ok(); + r } ``` @@ -785,6 +822,10 @@ It must be placed **after** all your `dispatcher!`, `pack!`, `#[chain]`, `#[rend Here's a complete, runnable program: ```rust +use mingling::macros::pack; +use mingling::prelude::*; +use std::io::Write; + dispatcher!("greet", CMDGreet => EntryGreet); fn main() { @@ -806,8 +847,10 @@ fn handle_greet(args: EntryGreet) -> Next { } #[renderer] -fn render_greeting(greeting: ResultGreeting) { - r_println!("Hello, {}!", *greeting); +fn render_greeting(greeting: ResultGreeting) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Hello, {}!", *greeting).ok(); + result } gen_program!(); @@ -825,7 +868,7 @@ Hello, Alice! 🗺️ Roadmap 🗺️ </h1> -- [ ] Milestone.1 "MVP" +- [x] Milestone.1 "MVP" 🎉 - [x] [[0.1.4](https://docs.rs/mingling/0.1.4/mingling/)] [`core`] [`structural_renderer`] **Mingling** can render data into serializable formats via `--json` and `--yaml` flags - [x] [[0.1.5](https://docs.rs/mingling/0.1.5/mingling/)] [`core`] [`comp`] **Mingling** can dynamically invoke itself to provide completions for shells like `bash`, `zsh`, `fish`, and `pwsh` - [x] [[0.1.6](https://docs.rs/mingling/0.1.6/mingling/)] [`core`] [`comp`] **Mingling** can gather more context for smarter completions @@ -835,13 +878,15 @@ Hello, Alice! - [x] [[0.1.8](https://docs.rs/mingling/0.1.8/mingling/)] [`core`] [`dispatch_tree`] Converts the subcommand list into a prefix tree to improve command matching speed - [x] [[0.1.9](https://docs.rs/mingling/0.1.9/mingling/)] [`core`] [`dev_toolkits`] Provides debugging interfaces for developers to capture invocation information when issues arise (`InvokeStackDisplay`) (indirectly implemented via `ProgramHook`) - [x] [[0.1.9](https://docs.rs/mingling/0.1.9/mingling/)] [`core`] [`repl`] Provides REPL capability (`program.exec_repl();`) - - [ ] [**0.2.0**] Complete documentation, tests, and examples - + - [x] [[0.2.0](https://docs.rs/mingling/0.2.0/mingling/)] Complete documentation, tests, and examples - [ ] Milestone.2 "More Comfortable Dev and User Experience" - - [ ] [**0.2.1**] [`macros`] `r_println!` in `#[chain]` support. - - [ ] [**0.2.5**] [`mling`] Helpdoc Maker - - [ ] [**0.2.8**] [`picker`] A more efficient and intelligent argument parser - + - [ ] [`mling` / `mingling-cli`] + - [ ] **Mingling** Linter + - [ ] **Mingling** Project Generator + - [ ] **Mingling** Program Installer & Manager (For development) + - [ ] Helpdoc Editor + - [ ] [`picker`] A more efficient and intelligent argument parser + - [x] [`macros`] Remove `r_print!` / `r_println!` macros - [ ] Milestone.3 "Unplanned" - [ ] ... diff --git a/dev_tools/scripts/doc.ps1 b/dev_tools/scripts/doc.ps1 deleted file mode 100755 index 987f0de..0000000 --- a/dev_tools/scripts/doc.ps1 +++ /dev/null @@ -1 +0,0 @@ -cargo doc --workspace --no-deps --features builds,structural_renderer,repl,comp,parser,clap,extra_macros --open diff --git a/dev_tools/scripts/doc.sh b/dev_tools/scripts/doc.sh deleted file mode 100755 index 5e8a311..0000000 --- a/dev_tools/scripts/doc.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -cargo doc --workspace --no-deps --features builds,structural_renderer,repl,comp,parser,clap,extra_macros --open diff --git a/docs/_ABOUT_TRANSLATION.md b/docs/_ABOUT_TRANSLATION.md deleted file mode 100644 index cc33128..0000000 --- a/docs/_ABOUT_TRANSLATION.md +++ /dev/null @@ -1,25 +0,0 @@ -# Translation Style Guide - -## 1. Tone & Voice - -- **保持原语气** (Preserve original tone): Maintain the author's attitude, formality, and emotional register exactly as in the source. -- **近似词替换** (Synonymous substitution): Use words with close or equivalent meaning where direct translation is awkward or unnatural. - -## 2. Vocabulary & Abbreviation - -- **缩写** (Abbreviation): Apply standard English abbreviations (e.g., _info_ for information, _dept_ for department) to avoid overlong words, but only when clarity is not sacrificed. -- **简明表述** (Concise expression): Prefer shorter, more common alternatives (e.g., _use_ over _utilize_, _help_ over _facilitate_) unless the original tone demands formality. - -## 3. Structural Rules - -- **段落一致** (Paragraph integrity): Keep the original paragraph breaks and line spacing. -- **标记保留** (Tag preservation): Any inline Markdown formatting (bold, italic, code, links, lists) must be replicated exactly in translation. -- **例示** (Example): - - 原句: “请保持专业语气,但避免使用过长的学术词汇。” - - 译文: “Keep a prof. tone, but avoid long academic words.” -- **最小化改动** (Minimal diff): When translating or syncing English content against a known Chinese original, if the Chinese original's meaning is extremely close to the current English meaning, do not modify the English text. This is to keep git diffs friendly (only modify parts that have truly changed). - -## 4. Exceptions - -- If a term has no common abbreviation, use the full word. -- If preserving tone requires a longer phrase, prioritize tone over brevity. diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 47f46b8..84e36a2 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -1,5 +1,26 @@ - [Welcome!](README) * [Getting Started](pages/1-getting-started) +* [Declare a Dispatcher](pages/2-define-a-dispatcher) +* [Declare a Chain](pages/3-define-a-chain) +* [Rendering Results](pages/4-render-result) +* [Multi-Command Program](pages/5-multiple-commands) +* [Parsing Arguments with Picker](pages/6-argument-parse-picker) +* [Parsing Arguments with Clap](pages/7-argument-parse-clap) +* [Program Setup](pages/8-setup-and-resources) +* [Error Handling](pages/9-error-handling) +* [Help Info](pages/10-help) +* [Using the Resource System](pages/11-resource-system) +* [Exit Code Control](pages/12-exit-code) +* [Hook System](pages/13-hook) +* [Testing Your Program](pages/14-testing) +* Advanced + * [Completion](pages/advanced/1-completion) + * [Structural Rendering](pages/advanced/2-structural-renderer) +* Core Concepts + * [The Pipeline](pages/concepts/1-the-pipeline) + * [Resource System](pages/concepts/2-resource) + * [AnyOutput Mechanism](pages/concepts/3-any-output) + * [About ProgramCollect](pages/concepts/4-program-collect) * Other * [Features](pages/other/features) * [Naming Conventions](pages/other/naming_rule) diff --git a/docs/_zh_CN/_sidebar.md b/docs/_zh_CN/_sidebar.md index 620b11c..80645c0 100644 --- a/docs/_zh_CN/_sidebar.md +++ b/docs/_zh_CN/_sidebar.md @@ -1,5 +1,26 @@ - [Welcome!](README) * [起步](pages/1-getting-started) +* [声明一个分发器](pages/2-define-a-dispatcher) +* [声明一个链](pages/3-define-a-chain) +* [将结果渲染](pages/4-render-result) +* [多命令程序](pages/5-multiple-commands) +* [使用 Picker 完成参数解析](pages/6-argument-parse-picker) +* [使用 Clap 完成参数解析](pages/7-argument-parse-clap) +* [程序装配](pages/8-setup-and-resources) +* [错误处理](pages/9-error-handling) +* [帮助信息](pages/10-help) +* [使用资源系统](pages/11-resource-system) +* [退出码控制](pages/12-exit-code) +* [钩子系统](pages/13-hook) +* [测试你的程序](pages/14-testing) * 其他 * [特性](pages/other/features) * [命名规范](pages/other/naming_rule) +* 核心概念 + * [基础管线](pages/concepts/1-the-pipeline) + * [资源系统](pages/concepts/2-resource) + * [任意输出机制](pages/concepts/3-any-output) + * [关于 ProgramCollect](pages/concepts/4-program-collect) +* 进阶 + * [补全](pages/advanced/1-completion) + * [结构化渲染](pages/advanced/2-structural-renderer) diff --git a/docs/_zh_CN/index.html b/docs/_zh_CN/index.html index 7adaa98..ad21062 100644 --- a/docs/_zh_CN/index.html +++ b/docs/_zh_CN/index.html @@ -56,7 +56,7 @@ auto2top: true, loadSidebar: true, maxLevel: 0, - subMaxLevel: 3, + subMaxLevel: 0, search: { placeholder: "Search", noData: "No matches found.", diff --git a/docs/_zh_CN/pages/1-getting-started.md b/docs/_zh_CN/pages/1-getting-started.md index 03bb1be..84870b2 100644 --- a/docs/_zh_CN/pages/1-getting-started.md +++ b/docs/_zh_CN/pages/1-getting-started.md @@ -13,7 +13,7 @@ cd my-cli ```toml [dependencies.mingling] -version = "0.2.0" +version = "0.3.0" features = [] ``` @@ -25,7 +25,7 @@ features = [] ```toml [dependencies.mingling] -version = "0.2.0" +version = "0.3.0" features = [ "parser", "comp", diff --git a/docs/_zh_CN/pages/10-help.md b/docs/_zh_CN/pages/10-help.md new file mode 100644 index 0000000..8cee2aa --- /dev/null +++ b/docs/_zh_CN/pages/10-help.md @@ -0,0 +1,73 @@ +<h1 align="center">帮助信息</h1> +<p align="center"> + 为命令添加 --help 支持 +</p> + +没有帮助信息的 CLI 不是好 CLI。 + +Mingling 里用 `#[help]` 宏给命令添加帮助文本。 + +## 最简单的帮助 + +直接给 Entry 写一个帮助函数: + +```rust +@@@use mingling::macros::help; +@@@dispatcher!("greet", CMDGreet => EntryGreet); +#[help] +fn help_greet(_entry: EntryGreet) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Usage: greet [name]").ok(); + writeln!(r, "Say hello to someone.").ok(); + r +} +``` + +> [!NOTE] +> 帮助函数同样通过 `writeln!` 向 `RenderResult` 写入内容,因为 `#[help]` 遵循渲染管线 —— 它是由 `--help` 标志提前触发的短路渲染,而不是管线之外的逻辑。 + +## 全局帮助 + +你也可以为 `ErrorDispatcherNotFound` 写帮助,作为"根帮助": + +```rust +@@@use mingling::macros::help; +// 用户直接输入 --help 时触发 +#[help] +fn help_root(entry: ErrorDispatcherNotFound) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Usage: my-cli <command>").ok(); + writeln!(r, "Commands:").ok(); + writeln!(r, " greet Say hello").ok(); + r +} +``` + +> [!TIP] +> `ErrorDispatcherNotFound` 是 `gen_program!()` 自动生成的类型,代表"没有匹配到任何命令"的情况。为它写 `#[help]` 就是给程序的根命令加帮助。 + +## 需要 Setup 配合 + +要让 `--help` 正常工作,需要在 `main` 里加上 `BasicProgramSetup`: + +```rust +@@@use mingling::macros::help; +@@@use mingling::setup::BasicProgramSetup; +@@@dispatcher!("greet", CMDGreet => EntryGreet); +fn main() { + let mut program = ThisProgram::new(); + program.with_setup(BasicProgramSetup); + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} +``` + +`BasicProgramSetup` 内置了 `HelpFlagSetup`,它的作用仅仅是把 `program.user_context.help` 设为 `true`。 + +真正把请求路由到 `#[help]` 函数的是 `gen_program!()` 生成的代码 —— 它在调度时检查这个标记,如果为 `true` 就走帮助渲染路径,不经过 Chain。 + +不加 `BasicProgramSetup` 的话,`--help` 只是一个普通参数,会被当成 Entry 的输入传给 Chain。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/_zh_CN/pages/11-resource-system.md b/docs/_zh_CN/pages/11-resource-system.md new file mode 100644 index 0000000..152e5d6 --- /dev/null +++ b/docs/_zh_CN/pages/11-resource-system.md @@ -0,0 +1,93 @@ +<h1 align="center">使用资源系统</h1> +<p align="center"> + 手把手带你使用资源 +</p> + +资源是 Mingling 中管理全局状态的机制。任何实现了 `Default + Clone` 的类型都可以成为资源。 + +## 定义一个资源 + +```rust +// 只要实现 Default + Clone,就可以作为资源使用 +#[derive(Default, Clone)] +struct ResCurrentDir(String); + +// 注册到 Program +fn main() { + let mut program = ThisProgram::new(); + program.with_resource(ResCurrentDir(".".into())); + program.exec_and_exit(); +} +``` + +因为 `ResCurrentDir` 同时实现了 `Default` 和 `Clone`,框架会自动为它实现 `ResourceMarker` trait,无需手动 impl。 + +## 注入并使用 + +在 Chain 或 Renderer 中,只需在参数列表里声明你要的资源: + +```rust +@@@#[derive(Default, Clone)] +@@@struct ResCurrentDir(String); +@@@dispatcher!("pwd", CMDPrintWorkingDir => EntryPrintWorkingDir); +@@@pack!(ResultPath = String); +// 通过 &T 注入只读资源 +#[chain] +fn handle_pwd(_args: EntryPrintWorkingDir, cwd: &ResCurrentDir) -> Next { + ResultPath::new(cwd.0.clone()).to_render() +} + +#[renderer] +fn render_path(result: ResultPath) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "{}", *result).ok(); + r +} +``` + +## 修改资源 + +用 `&mut T` 注入可修改资源: + +```rust +@@@#[derive(Default, Clone)] +@@@struct ResVisitCount(u32); +@@@dispatcher!("visit", CMDVisit => EntryVisit); +@@@pack!(ResultDone = ()); +#[chain] +fn handle_visit(_args: EntryVisit, counter: &mut ResVisitCount) -> Next { + counter.0 += 1; + ResultDone::default() +} + +#[renderer] +fn render_done(_done: ResultDone, counter: &ResVisitCount) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "visit count is : {}", counter.0).ok(); + r +} +``` + +## 多个资源同用 + +Chain 可以同时注入任意多个资源,框架按类型自动匹配: + +```rust +@@@#[derive(Default, Clone)] struct ResConfig(String); +@@@#[derive(Default, Clone)] struct ResCounter(u32); +@@@dispatcher!("test", CMDTest => EntryTest); +@@@pack!(ResultDone = ()); +// 同时注入只读 + 可修改 +#[chain] +fn handle_test(_args: EntryTest, config: &ResConfig, counter: &mut ResCounter) -> Next { + println!("config: {}", config.0); + counter.0 += 1; + ResultDone::default().to_render() +} +``` + +如果要深入了解 `ResourceMarker`、`LazyRes` 惰性加载等进阶内容,可以查看 [核心概念:资源系统](pages/concepts/2-resource)。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/_zh_CN/pages/12-exit-code.md b/docs/_zh_CN/pages/12-exit-code.md new file mode 100644 index 0000000..7c55b60 --- /dev/null +++ b/docs/_zh_CN/pages/12-exit-code.md @@ -0,0 +1,70 @@ +<h1 align="center">退出码控制</h1> +<p align="center"> + 如何使用资源系统管理程序退出码 +</p> + +程序退出时给 shell 一个正确的退出码是 CLI 的基本素养 + +。Mingling 提供了开箱即用的 `ExitCodeSetup`,配合 `ResExitCode` 资源,让退出码控制变得极其简单。 + +## 启用 ExitCodeSetup + +```rust +@@@use mingling::prelude::*; +@@@use mingling::setup::ExitCodeSetup; +fn main() { + let mut program = ThisProgram::new(); + program.with_setup(ExitCodeSetup::default()); +@@@ program.exec_and_exit(); +} +``` + +`ExitCodeSetup` 做了两件事: + +1. 注册 `ResExitCode` 资源(默认值为 `0`) +2. 注册一个 `finish` hook,在程序退出前读取 `ResExitCode` 的值作为最终退出码 + +## 修改退出码 + +在 Chain 或 Renderer 中通过 `ResExitCode` 注入来修改退出码: + +```rust +@@@use mingling::res::ResExitCode; +@@@use mingling::setup::ExitCodeSetup; +@@@pack!(EntryCheck = Vec<String>); +#[chain] +fn handle_check(_args: EntryCheck, ec: &mut ResExitCode) { + // 检查失败的时候修改退出码资源 + ec.exit_code = 1; +} +``` + +> [!TIP] +> `ResExitCode` 就是一个 `struct ResExitCode { pub exit_code: i32 }`。`&mut ResExitCode` 注入后直接改字段即可。 + +## `Program` 的三种执行方式 + +`Program` 提供了三种执行方式(不包括 `repl` 特性下的 `exec_repl`): + +| 方式 | 行为 | +| ------------------------------- | -------------------------------------------------------------------------- | +| `program.exec_and_exit()` | 执行并直接以退出码终止进程 | +| `program.exec()` | 执行后返回 `i32` 退出码,由调用方决定怎么处理 | +| `program.exec_without_render()` | 返回 `Result<RenderResult, ProgramExecuteError>`,可读取内部的 `exit_code` | + +```rust +@@@use mingling::setup::ExitCodeSetup; +fn main() { + let mut program = ThisProgram::new(); + program.with_setup(ExitCodeSetup::default()); + + // 获取退出码自行处理 + let exit_code = program.exec(); + std::process::exit(exit_code); +} +@@@gen_program!(); +``` + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/_zh_CN/pages/13-hook.md b/docs/_zh_CN/pages/13-hook.md new file mode 100644 index 0000000..6d6018a --- /dev/null +++ b/docs/_zh_CN/pages/13-hook.md @@ -0,0 +1,107 @@ +<h1 align="center">钩子系统</h1> +<p align="center"> + 如何使用 ProgramHook 向程序内部插入行为 +</p> + +Hook 让你在管线的各个生命周期节点插入自定义逻辑 —— 在 dispatch 之前、chain 之后、render 前后、程序退出时 …… + +你可以把横切关注点(日志、鉴权、指标收集)写在 hook 里,而不是散落在各处的业务代码中。 + +## 基本用法 + +`ProgramHook` 采用 builder 模式构造: + +```rust +@@@use mingling::hook::ProgramHook; +fn main() { + let mut program = ThisProgram::new(); + program.with_hook( + ProgramHook::empty() + .on_pre_chain(|info| { + println!("before chain: {}", info.input); + }) + .on_post_render(|info| { + println!("after render: {}", info.result); + }), + ); + program.exec_and_exit(); +} +``` + +> [!TIP] +> `ProgramHook::empty()` 创建一个空 hook,然后链式调用 `.on_*()` 方法注册你关心的生命周期节点。没有注册的节点不会执行。 + +## 生命周期节点 + +Hook 覆盖了管线的完整生命周期: + +| 阶段 | Hook | 触发时机 | +| ------------ | ------------------ | ------------- | +| **Dispatch** | `on_begin` | 执行开始 | +| | `on_pre_dispatch` | Dispatch 之前 | +| | `on_post_dispatch` | Dispatch 之后 | +| **Chain** | `on_pre_chain` | Chain 执行前 | +| | `on_post_chain` | Chain 执行后 | +| **Render** | `on_pre_render` | Render 执行前 | +| | `on_post_render` | Render 执行后 | +| **Finish** | `on_finish` | 程序退出前 | + +每个 hook 回调接收对应的 `Hook*Info` 结构体,里面包含当前上下文的信息(输入类型、参数、渲染结果等)。 + +## 实际例子:记录操作日志 + +```rust +@@@use mingling::prelude::*; +@@@use mingling::hook::ProgramHook; +@@@ +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +@@@ +@@@#[chain] fn handle_greet(args: EntryGreet) -> Next { +@@@ ResultName::new(args.inner.first().cloned().unwrap_or_default()).to_render() +@@@} +@@@#[renderer] fn render_name(r: ResultName) -> RenderResult { RenderResult::new() } +fn main() { + let mut program = ThisProgram::new(); + + // 记录每次 chain 执行前后的信息 + program.with_hook( + ProgramHook::empty() + .on_pre_chain(|info| { + eprintln!("[hook] executing chain for: {}", info.input); + }) + .on_post_chain(|info| { + eprintln!("[hook] chain output: {}", info.output.member_id); + }), + ); + + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} +``` + +运行效果: + +```text +[hook] executing chain for: EntryGreet +[hook] chain output: ResultName +Hello, World! +``` + +## 通过 Hook 控制行为 + +Hook 不仅用来 "看",还可以用 `ProgramControlUnit` 改变程序行为: + +| 变体 | 效果 | +| -------------------------- | ----------------------------------------- | +| `Continue` | 什么都不做,继续执行 | +| `OverrideExitCode(i32)` | 覆盖退出码 | +| `RouteToChain(AnyOutput)` | 将当前数据替换为新的,重新进入 Chain 循环 | +| `RouteToRender(AnyOutput)` | 跳过后续 Chain,直接渲染 | + +> [!NOTE] +> Hook 可以注册多个,按注册顺序执行。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/_zh_CN/pages/14-testing.md b/docs/_zh_CN/pages/14-testing.md new file mode 100644 index 0000000..621d43e --- /dev/null +++ b/docs/_zh_CN/pages/14-testing.md @@ -0,0 +1,130 @@ +<h1 align="center">测试你的程序</h1> +<p align="center"> + 为 Chain 和 Renderer 编写单元测试 +</p> + +管线模型附带的一个好处就是 **可测试性**。 + +Chain 只是一个接收输入、返回输出的函数,Renderer 也只是接收输入、写入内容的函数 —— 没有全局状态的黑魔法,测试起来很直接。 + +## 测试 Renderer + +Renderer 是最容易测试的——调用函数,断言返回结果: + +```rust +@@@pack!(ResultName = String); +#[renderer] +fn render_greet(result: ResultName) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Hello, {}!", *result).ok(); + r +} + +#[test] +fn test_render_name() { + let result = render_name(ResultName::new("Alice".to_string())); + assert_eq!(result.to_string().as_str(), "Hello, Alice!\n"); +} +``` + +注意到 Renderer 的返回值改成了 `-> String`——`#[renderer]` 会把 `RenderResult` 自动转换成你指定的返回类型(默认是 `()`)。返回 `String` 后你就可以直接断言输出内容了。 + +## 测试 Chain + +测试 Chain 稍微复杂一点,因为它的返回值是 `Next`(实际是 `impl Into<ChainProcess<ThisProgram>>`)。需要用框架提供的断言宏: + +```rust +@@@use mingling::{assert_member_id, assert_render_result, unpack_chain_process}; +@@@dispatcher!("hello", CMDHello => EntryHello); +@@@pack!(ResultName = String); +@@@pack!(ErrorNoName = ()); +@@@#[chain] +@@@fn handle_hello(args: EntryHello) -> Next { +@@@ let name = args.inner.first().cloned().unwrap_or_default(); +@@@ if name.is_empty() { +@@@ ErrorNoName::default().to_render() +@@@ } else { +@@@ ResultName::new(name).to_render() +@@@ } +@@@} +#[test] +fn test_handle_hello_with_name() { + let chain_process = handle_hello(EntryGreet::new(vec!["Alice".to_string()])).into(); + // 断言这是一个渲染结果(不是继续 chain) + assert_render_result!(chain_process); + // 断言 member_id 是 ResultName + assert_member_id!(chain_process, ResultName); + // 解包出内部值 + let result_name = unpack_chain_process!(chain_process, ResultName); + assert_eq!(result_name.inner, "Alice"); +} +``` + +三个测试宏的作用: + +| 宏 | 功能 | +| ----------------------- | --------------------------------------------- | +| `assert_render_result!` | 断言 Chain 返回的是渲染路径(而非继续 chain) | +| `assert_member_id!` | 断言返回值的成员 ID 是某个类型 | +| `unpack_chain_process!` | 从 ChainProcess 中解包出原始类型 | + +## 用 entry! 宏构造数据 + +如果启用了 `extra_macros`,可以用 `entry!` 快速构造 Entry: + +```rust +// Features: ["extra_macros"] + +@@@use mingling::{assert_member_id, unpack_chain_process}; +@@@use mingling::macros::entry; +@@@dispatcher!("hello", CMDHello => EntryHello); +@@@pack!(ResultName = String); +@@@#[chain] +@@@fn handle_hello(args: EntryHello) -> Next { +@@@ let name = args.inner.first().cloned().unwrap_or_default(); +@@@ ResultName::new(name).to_render() +@@@} +#[test] +fn test_with_entry_macro() { + // entry! 从字符串字面量构造 Entry + let entry = entry!("--name", "Alice"); + let chain_process = handle_hello(entry).into(); + let result_name = unpack_chain_process!(chain_process, ResultName); + assert_eq!(result_name.inner, "Alice"); +} +``` + +## 测试资源注入 + +如果 Chain 使用了资源,测试时需要提供资源实例: + +```rust +@@@use mingling::{assert_render_result, unpack_chain_process}; +@@@#[derive(Default, Clone)] +@@@struct ResPrefix(String); +@@@dispatcher!("hello", CMDHello => EntryHello); +@@@pack!(ResultGreeting = String); +@@@ +#[chain] +fn handle_hello(args: EntryHello, prefix: &ResPrefix) -> Next { + let name = args.inner.first().cloned().unwrap_or_default(); + ResultGreeting::new(format!("{}, {}", prefix.0, name)).to_render() +} + +#[test] +fn test_handle_with_resource() { + // 资源需要在测试中手动传入 + let result = handle_hello( + EntryHello::new(vec!["World".to_string()]), + &ResPrefix("Hello".to_string()), + ); + let greeting = unpack_chain_process!(result, ResultGreeting, ThisProgram); + assert_eq!(greeting.inner, "Hello, World"); +} +``` + +管线模型让测试变得简单:每个 Chain 和 Renderer 都是相对独立的函数,构造输入、断言输出即可。 + +<p align="center" style="font-size: 0.85em; color: clear;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/_zh_CN/pages/2-define-a-dispatcher.md b/docs/_zh_CN/pages/2-define-a-dispatcher.md new file mode 100644 index 0000000..0afd911 --- /dev/null +++ b/docs/_zh_CN/pages/2-define-a-dispatcher.md @@ -0,0 +1,103 @@ +<h1 align="center">声明一个分发器</h1> +<p align="center"> + 使用 dispatcher! 宏声明命令,并注册 +</p> + +Mingling 的管线从 Dispatcher 开始。 + +它的工作很简单:**匹配用户输入的命令,把参数包装成一个 Entry 类型**。 + +## `dispatcher!` 宏 + +`dispatcher!` 宏会同时生成两个类型: + +| 生成物 | 用途 | +| ----------- | ----------------------------------------------- | +| `CMDType` | 分发器本身,需要注册到 Program | +| `EntryType` | 入口类型,包裹 `Vec<String>`,作为 Chain 的输入 | + +写法是固定的三个部分: + +```rust +dispatcher!("命令路径", 分发器类型 => 入口类型); +``` + +看一个具体的例子: + +```rust +dispatcher!("greet", CMDGreet => EntryGreet); +``` + +> [!NOTE] +> 命令名(`"greet"`)会自动转换为 kebab-case。即使你写 `"GreetUser"`,匹配时也会变成 `greet-user`。 + +## 注册到 Program + +有了分发器之后,需要告诉 Program 它的存在: + +```rust +@@@ dispatcher!("greet", CMDGreet => EntryGreet); +@@@ fn main() { +@@@ let mut program = ThisProgram::new(); +// 注册分发器 +program.with_dispatcher(CMDGreet); +@@@ } +@@@ gen_program!(); +``` + +> [!TIP] +> 如果命令多了,可以用 `with_dispatchers` 一次注册多个:`program.with_dispatchers((CMDGreet, CMDAdd, CMDRemoteRm))`。 + +## 多级命令 + +如果你的程序有层级结构——比如 `remote add`、`remote rm`——只需要在命令名里加点号分隔: + +```rust +dispatcher!("remote.add", CMDRemoteAdd => EntryRemoteAdd); +dispatcher!("remote.rm", CMDRemoteRm => EntryRemoteRm); +``` + +用户在终端输入 `remote add` 时,Mingling 会依次匹配 `remote` 和 `add` 两个层级。 + +## 入口类型 `EntryGreet` + +你可能会好奇 `EntryGreet` 里面到底有什么。它本质上就是一个包装了 `Vec<String>` 的结构体: + +```rust +// 示意,dispatcher! 宏实际生成的代码 +pub struct EntryGreet { + pub inner: Vec<String>, +} +``` + +用户在命令行输入 `greet Alice Bob`,`EntryGreet.inner` 就是 `vec!["Alice", "Bob"]`。 + +> [!IMPORTANT] +> Entry 的 `inner` 只包含 **匹配后剩余的参数**。 +> +> 以 `remote add origin` 为例,`remote` 和 `add` 用于匹配命令路径,只有 `origin` 会进入 `EntryRemoteAdd.inner`。 + +## 进阶:隐式声明 + +以上是标准写法。如果你启用了 `extra_macros` 特性,还可以更简洁: + +```rust +// Features: ["extra_macros"] +// 省略 CMDType 和 EntryType,名字自动推导 + dispatcher!("greet"); +// dispatcher!("greet", CMDGreet => EntryGreet); +``` + +这种写法会自动生成 `CMDGreet` 和 `EntryGreet`,效果跟显式声明完全一样。 + +不过在教程阶段,我们继续用显式写法——更清晰,也不依赖额外特性。 + +详见[特性列表](pages/other/features)。 + +## 下一步 + +接下来我们写一个 Chain 来接收 Entry,处理真正的业务逻辑。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/_zh_CN/pages/3-define-a-chain.md b/docs/_zh_CN/pages/3-define-a-chain.md new file mode 100644 index 0000000..7ac5c60 --- /dev/null +++ b/docs/_zh_CN/pages/3-define-a-chain.md @@ -0,0 +1,131 @@ +<h1 align="center">声明一个链</h1> +<p align="center"> + 使用 chain 宏声明链,并承接 Entry 输入 +</p> + +上一节我们声明了 `dispatcher!("greet", CMDGreet => EntryGreet)` + +现在用户输入 `greet` 时会被匹配并包装成 `EntryGreet`。 + +但拿到 Entry 之后呢? + +我们需要一个 Chain 来处理它。 + +## `#[chain]` 宏 + +`#[chain]` 用来标记一个处理函数,格式非常直接: + +```rust +@@@dispatcher!("greet", CMDGreet => EntryGreet); +pack!(ResultName = String); + +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + // args 就是用户输入经过匹配后剩下的参数 + let name = args.inner.first().cloned().unwrap_or_else(|| "World".to_string()); + // 把结果包装成 Next,告诉调度器下一步去哪 + ResultName::new(name) +} +``` + +注意到了吗? + +Chain 函数签名里写着它需要什么——`args: EntryGreet` + +然后用 `ResultName::new(name)` 返回一个新类型。 + +这个返回的 `Next` 会展开成 `impl Into<ChainProcess<ThisProgram>>`。 + +> [!TIP] +> 想知道 `Into<ChainProcess<G>>` 是怎么工作的? +> +> 可以去 [任意输出机制](pages/concepts/3-any-output) 章节了解 `ChainProcess`。 + +## `pack!` 宏 + +你大概猜到了,`pack!(ResultName = String)` 定义了一个管线中传递的类型: + +```rust +// pack!(ResultName = String) 大概生成了这样的代码 + +#[derive(Groupped)] +pub struct ResultName { + pub inner: String, +} +``` + +你可以把它理解为一个 打了标签的 `String`。 + +调度器通过这个标签来精确路由,确保数据不会混淆 —— 比如发给 `RenderGreet` 的数据不会被误传给 `RenderError`。 + +> [!NOTE] +> 与简单的类型别名 (`type`) 不同,`pack!` 会生成一个全新的类型,拥有独立的 `TypeId`。 + +命名上推荐这样的习惯: + +| 角色 | 命名模式 | 示例 | +| -------- | ---------------- | -------------------- | +| 入口 | `Entry` + 命令名 | `EntryGreet` | +| 中间状态 | `State` + 描述 | `StateParsedArgs` | +| 最终结果 | `Result` + 描述 | `ResultGreetSomeone` | +| 错误 | `Error` + 描述 | `ErrorUserNotFound` | + +详见 [命名规范](pages/other/naming_rule),不过现在你只需要记住:**用 `pack!` 给你的数据取一个有意义的名字**。 + +## 从 Entry 中提取参数 + +`EntryGreet` 的 `inner` 是一个 `Vec<String>`,你可以在 Chain 里自由地处理它: + +```rust +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + // 取第一个参数,没有就用默认值 + let name = args + .inner + .first() + .cloned() + .unwrap_or_else(|| "World".to_string()); + + ResultName::new(name) +} +``` + +如果你启用了 `parser` 特性,还可以用 `Picker` 做更灵活的参数提取,不过那是后话了。 + +## 组合起来 + +现在把 Dispatcher 和 Chain 连在一起: + +```rust +// 1. 声明命令 +dispatcher!("greet", CMDGreet => EntryGreet); + +// 2. 声明管线中的数据类型 +pack!(ResultName = String); + +// 3. 处理逻辑 +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + let name = args.inner + .first() + .cloned() + .unwrap_or_else(|| "World".to_string()); + ResultName::new(name) +} + +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} + +gen_program!(); +``` + +不过这段代码还没写完 —— 我们只有 Dispatcher 和 Chain,还差最后一步:**把结果渲染出来**。这就是下一篇要讲的 Renderer。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/_zh_CN/pages/4-render-result.md b/docs/_zh_CN/pages/4-render-result.md new file mode 100644 index 0000000..7cf2d2b --- /dev/null +++ b/docs/_zh_CN/pages/4-render-result.md @@ -0,0 +1,151 @@ +<h1 align="center">将结果渲染</h1> +<p align="center"> + 使用 renderer 宏声明渲染器,将结果输出 +</p> + +现在,我们创建了 Dispatcher 和 Chain,也通过 `pack!` 产出了一个 Result 类型。最后一步:**把结果展示给用户**。 + +## `#[renderer]` 宏 + +跟 `#[chain]` 类似,`#[renderer]` 用于标记一个输出函数: + +```rust +use std::io::Write; + +@@@pack!(ResultName = String); +#[renderer] +fn render_name(name: ResultName) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Hello, {}!", *name).ok(); + result +} +``` + +Renderer 接收 Chain 产出的结果,然后返回一个 `RenderResult`。在函数内部,创建 `RenderResult`,用 `write!` / `writeln!`(来自 [`std::io::Write`](https://doc.rust-lang.org/std/io/trait.Write.html))写入内容,最后返回它。 + +## `RenderResult` 类型 + +`RenderResult` 是一个缓冲区类型,持有渲染后的文本和退出码。它不是直接输出到终端,而是把内容写入缓冲区。这样做的好处是: + +1. **持有退出码**——你可以设置程序以特定退出码结束 +2. **方便测试**——可以捕获渲染结果做断言 +3. **便于后处理**——你可以将结果捕获,统一进行文本后处理 + +## 完整的可运行程序 + +把三篇教程的内容合在一起,你的第一个 Mingling 程序就完整了: + +```rust +use std::io::Write; + +// 1. 用 Dispatcher 声明命令 +dispatcher!("greet", CMDGreet => EntryGreet); + +// 2. 用 pack! 声明结果数据 +pack!(ResultName = String); + +// 3. 用 Chain 处理逻辑 +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + let name = args.inner + .first() + .cloned() + .unwrap_or_else(|| "World".to_string()); + ResultName::new(name) +} + +// 4. 用 Renderer 输出结果 +#[renderer] +fn render_name(name: ResultName) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Hello, {}!", *name).ok(); + result +} + +// 5. 在 main 函数内装配程序并运行 +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} + +// 6. 使用 gen_program! 生成完整程序 +gen_program!(); +``` + +## 跑起来试试 + +```bash +~# cargo run -- greet Alice +``` + +```text +Hello, Alice! +``` + +试试不给参数: + +```bash +~# cargo run -- greet +``` + +```text +Hello, World! +``` + +试试不存在的命令: + +```bash +cargo run -- great +``` + +```text +# 什么也没输出! +``` + +## 补上 Fallback + +`gen_program!()` 自动生成了一个 `ErrorDispatcherNotFound` 类型,包裹 `Vec<String>`——它存的是用户输入的那些没匹配到的命令。你只需要给它写一个 Renderer: + +```rust +use std::io::Write; + +#[renderer] +fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult { + let mut result = RenderResult::new(); + if err.inner.is_empty() { + writeln!(result, "Unknown command").ok(); + } else { + writeln!(result, "Command not found: \"{}\"", err.inner.join(" ")).ok(); + } + result +} +``` + +加上之后,再试试不存在的命令: + +```bash +cargo run -- great +``` + +```text +Command not found: "great" +``` + +## 恭喜 + +你完成了第一个完整的 Mingling 程序!来回顾一下学到的东西: + +| 概念 | 对应宏/函数 | 一句话 | +| -------- | ---------------- | -------------------------- | +| 声明命令 | `dispatcher!` | 告诉程序用户能输入什么 | +| 处理逻辑 | `#[chain]` | 收到参数后做什么 | +| 输出结果 | `#[renderer]` | 怎么把结果告诉用户 | +| 类型包装 | `pack!` | 给你的数据取个有意义的名字 | +| 程序入口 | `gen_program!()` | 自动生成管线的接线图 | + +在真实项目中你还会用到资源注入、hook、补全、REPL 等高级功能,不过核心骨架永远不变:**Dispatcher → Chain → Renderer**。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/_zh_CN/pages/5-multiple-commands.md b/docs/_zh_CN/pages/5-multiple-commands.md new file mode 100644 index 0000000..7d2617b --- /dev/null +++ b/docs/_zh_CN/pages/5-multiple-commands.md @@ -0,0 +1,113 @@ +<h1 align="center">多命令程序</h1> +<p align="center"> + 在一个程序里添加多个命令 +</p> + +真实世界的 CLI 很少只有一个命令。这篇我们来扩展之前的 greet 程序,加上第二个命令,看看多命令的程序长什么样。 + +## 添加第二个命令 + +继续在同一个项目里操作: + +```rust +// 声明两个命令 +dispatcher!("greet", CMDGreet => EntryGreet); +dispatcher!("add", CMDAdd => EntryAdd); + +pack!(ResultGreeting = String); +pack!(ResultSum = i32); + +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + let name = args.inner.first().cloned().unwrap_or_else(|| "World".to_string()); + ResultGreeting::new(name) +} + +#[chain] +fn handle_add(args: EntryAdd) -> Next { + let sum: i32 = args.inner.iter().filter_map(|s| s.parse::<i32>().ok()).sum(); + ResultSum::new(sum) +} + +#[renderer] +fn render_greet(result: ResultGreeting) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Hello, {}!", *result).ok(); + r +} + +#[renderer] +fn render_sum(result: ResultSum) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Sum: {}", *result).ok(); + r +} + +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatchers((CMDGreet, CMDAdd)); + program.exec_and_exit(); +} + +gen_program!(); +``` + +两个命令共享同一个管线模型,但各走各的: + +```text +> my-cli greet Alice +Hello, Alice! +> my-cli add 1 2 3 +Sum: 6 +``` + +## 注册多个分发器 + +注意到 `with_dispatchers` 了吗?当你需要注册多个分发器时,一次传一个元组就行: + +```rust +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@dispatcher!("add", CMDAdd => EntryAdd); +@@@pack!(ResultGreeting = String); +@@@pack!(ResultSum = i32); +@@@#[chain] fn handle_greet(_args: EntryGreet) -> Next { ResultGreeting::new("ok".into()) } +@@@#[renderer] fn render_greet(_greeting: ResultGreeting) -> RenderResult { RenderResult::new() } +@@@#[chain] fn handle_add(_args: EntryAdd) -> Next { ResultSum::new(0) } +@@@#[renderer] fn render_sum(_sum: ResultSum) -> RenderResult { RenderResult::new() } +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatchers((CMDGreet, CMDAdd)); + program.exec_and_exit(); +} +``` + +等价于一个个注册,效果一样。 + +> [!TIP] +> 元组最多支持 7 个分发器。超过 7 个时链式调用 `with_dispatcher` 就行。 + +## 子命令 + +多层级的命令也是同理——每个点号分隔的层级都只是名字的一部分: + +```rust +dispatcher!("remote.add", CMDRemoteAdd => EntryRemoteAdd); +dispatcher!("remote.rm", CMDRemoteRm => EntryRemoteRm); +``` + +每个子命令的 Entry、Chain、Renderer 完全独立,互不干扰。 + +## 数据类型的独立性 + +注意我们用了两个不同的 `pack!`: + +- `pack!(ResultGreeting = String)` +- `pack!(ResultSum = i32)` + +它们都是独立的类型,`gen_program!()` 会给它们分配不同的枚举变体。 + +调度器永远不会把 `ResultGreeting` 的数据送到 `render_sum` 去 —— **类型安全从命名那一刻就保证了**。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/_zh_CN/pages/6-argument-parse-picker.md b/docs/_zh_CN/pages/6-argument-parse-picker.md new file mode 100644 index 0000000..b41f839 --- /dev/null +++ b/docs/_zh_CN/pages/6-argument-parse-picker.md @@ -0,0 +1,397 @@ +<h1 align="center">使用 Picker 完成参数解析</h1> +<p align="center"> + 用 Picker 完成基本的参数解析 +</p> + +前面教程中我们都是手动从 `EntryGreet.inner`(`Vec<String>`)中提取参数。 + +```rust +@@@ fn main() { +@@@ let args : Vec<String> = vec![]; +let name = args.first().cloned().unwrap_or_else(|| "World".to_string()); +@@@ } +``` + +但是,对于参数较多的场景,这个方案就不够用了:Mingling 提供了 `Picker` —— 通过链式调用来提取和转换参数。 + +要启用 `Picker`,你需要修改 `Cargo.toml` + +```toml +# Cargo.toml +[dependencies.mingling] +features = ["parser"] +``` + +好了,让我们看看 `Picker` 的写法: + +```rust +// Features: ["parser"] +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); + +#[chain] +fn handle_greet_entry(prev: EntryGreet) -> Next { + let name = prev.pick_or((), "World").unpack(); + ResultName::new(name) +} +``` + +`AsPicker` 为所有可以转换为 `Vec<String>` 的类型实现了 `pick`、`pick_or`、`pick_or_route` 函数:它们可以语义化地从字符串列表中 **拾取 (Pick)** 参数,并转换为结构化数据。 + +对于上述示例中的代码: + +```rust +// Features: ["parser"] +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +@@@#[chain] +@@@fn handle_greet_entry(prev: EntryGreet) -> Next { +let name = prev.pick_or((), "World").unpack(); +@@@ResultName::new(name) +@@@} +``` + +它的语义为: + +```rust +// Features: ["parser"] +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +@@@#[chain] +@@@fn handle_greet_entry(prev: EntryGreet) { +@@@let name: String = + prev.pick_or((), "World").unpack(); +// ~~~~ ~~~~~~~ ~~ ~~~~~~~ ~~~~~~~~ +// | | | | |_ 解包为 String +// | | | |__________ 默认值为 "World" +// | | |______________ 取出第一个位置参数(不指定标志) +// | |______________________ 拾取或使用默认 +// |___________________________ 从前一个输入中 +@@@} +``` + +## 解析标志参数 + +若你的程序需要解析标志参数(例如 `greet --name Alice`),可以使用如下方式 + +```rust +// Features: ["parser"] +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); + +#[chain] +fn handle_greet_entry(prev: EntryGreet) -> Next { + let name = prev.pick_or(["--name", "-n"], "World").unpack(); + ResultName::new(name) +} +``` + +同理,它的语义为: + +```rust +// Features: ["parser"] +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +@@@#[chain] +@@@fn handle_greet_entry(prev: EntryGreet) { +@@@let name: String = + prev.pick_or(["--name", "-n"], "World").unpack(); +// ~~~~ ~~~~~~~ ~~~~~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~ +// | | | | |_ 解包为 String +// | | | |__________ 默认值为 "World" +// | | |____________________________ 取出 "--name" 或 "-n" 后面的参数 +// | |____________________________________ 拾取或使用默认 +// |_________________________________________ 从前一个输入中 +@@@} +``` + +## 关于 `.unpack()` + +你可能注意到了,`Picker` 在命令解析的最后,会执行一个 `.unpack()` 函数,它的作用是将前面解析出来的结果,转换为结构化信息。 + +对于只拾取了一次的数据来说,`.unpack()` 会返回单个数据,而对于多次拾取,`Picker` 则会返回元组: + +```rust +// Features: ["parser"] +@@@dispatcher!("test", CMDTest => EntryTest); +@@@pack!(ResultInfo = (String, u8, u32)); + +#[chain] +fn handle_test_entry(prev: EntryTest) -> Next { + let (name, age, id) = prev + .pick::<String>(["--name", "-n"]) + .pick::<u8>(["--age", "-a"]) + .pick::<u32>(["--id", "-I"]) + .unpack(); + + ResultInfo::new((name, age, id)) +} +``` + +> [!IMPORTANT] +> `Picker` 对解析顺序极其敏感,特别是位置参数:因为它是顺序解析的。若你需要解析位置参数,请确保解析前已拾取并消费所有 **标志参数**。 + +## 使用 `pick_or_route` 处理边界情况 + +就像那句老话:"永远不要相信你的用户"。为了应对必要参数缺失、输入类型不匹配等错误情况,`pick_or_route` 能将执行链路由到专门的错误处理类型上。 + +先来看一个简单示例 + +```rust +// Features: ["parser", "extra_macros"] +@@@use mingling::macros::route; +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +@@@pack!(ErrorNoName = ()); + +#[chain] +fn handle_greet_entry(prev: EntryGreet) -> Next { + let pick_result = prev + .pick_or_route(["--name", "-n"], ErrorNoName::default()) + .unpack(); + + // 使用 route! 宏展开 pick_result + let name = route!(pick_result); + ResultName::new(name).into() +} + +#[renderer] +fn render_greet(result: ResultName) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Hello, {}!", *result).ok(); + r +} +``` + +若使用 `pick_or_route`,写法会变得相对复杂:因为 `.unpack()` 不再直接返回参数,而是 `Result<Value, Route>`。 + +不过 **Mingling** 的 `extra_macros` 特性提供了简化展开的宏 `route!`,它不复杂,只是省略了一部分样板代码: + +```rust +// Features: ["parser", "extra_macros"] +@@@ pack!(ErrorFail = ()); +@@@ use mingling::macros::route; +@@@ fn func() -> mingling::ChainProcess<ThisProgram> { +@@@ let args: Vec<String> = vec![]; +@@@ let pick_result = args.pick_or_route::<String, _>((), ErrorFail::new(())).unpack(); +let name = route!(pick_result); +@@@ mingling::macros::empty_result!() +@@@ } +``` + +它展开为: + +```rust +// Features: ["parser", "extra_macros"] +@@@ pack!(ErrorFail = ()); +@@@ fn func() -> mingling::ChainProcess<ThisProgram> { +@@@ let args: Vec<String> = vec![]; +@@@ let pick_result = args.pick_or_route::<String, _>((), ErrorFail::new(())).unpack(); +let name = match pick_result { + Ok(r) => r, + Err(e) => return e.to_chain(), +}; +@@@ mingling::macros::empty_result!() +@@@ } +``` + +## 提取值的后处理 + +在您使用 `pick` 提取了用户输入后,可以使用 `after` 立刻处理该参数 + +```rust +// Features: ["parser"] +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); + +#[chain] +fn handle_greet_entry(prev: EntryGreet) -> Next { + let name = prev + .pick_or(["--name", "-n"], "World") + // 在提取出 --name 后,立刻格式化 + .after(|name: String| { + name.replace(['-', '_', '.'], " ") + .to_lowercase() + .trim() + .to_string() + }) + .unpack(); + + ResultName::new(name) +} +``` + +同样,你可以使用 `after_or_route` 来处理输入参数的格式错误 + +```rust +// Features: ["parser", "extra_macros"] +@@@use mingling::macros::route; +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +@@@pack!(ErrorNameTooLong = usize); + +#[chain] +fn handle_greet_entry(prev: EntryGreet) -> Next { + let pick_result = prev + .pick_or(["--name", "-n"], "World") + .after_or_route(|name: &String| { + if name.len() < 32 { + Ok(name.clone()) + } else { + Err(ErrorNameTooLong::new(name.len())) + } + }) + .unpack(); + let name = route!(pick_result); + + ResultName::new(name).into() +} + +#[renderer] +fn render_name_too_long(prev: ErrorNameTooLong) -> RenderResult { + let mut r = RenderResult::new(); + let len = *prev; + writeln!(r, "Error: name too long (length: {} > 32)", len).ok(); + r +} + +#[renderer] +fn render_name(prev: ResultName) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Hello, {}!", *prev).ok(); + r +} +``` + +## 布尔值解析 + +`Picker` 当然也可以解析布尔类型,但是布尔类型分为显式和隐式模式: + +| 模式 | 格式 | +| ---- | ----------------------------------- | +| 隐式 | `--confirmed` | +| 显式 | `--confirm true` 或 `--confirm yes` | + +- 使用 `.pick::<bool>(flag)` 时,采用隐式解析:只要标志存在即为 `true` +- 使用 `.pick::<Yes>(flag)` 或 `.pick::<True>(flag)` 时,采用显式解析 + +一般来说使用隐式解析即可,但在处理重要的确认行为时,显式逻辑更符合语义。 + +```rust +// Features: ["parser"] +@@@use mingling::parser::Yes; +@@@dispatcher!("test", CMDTest => EntryTest); +@@@pack!(ResultDone = ()); + +#[chain] +fn handle_entry(prev: EntryTest) -> Next { +@@@ let prev1 = prev.clone(); + let _confirmed: bool = prev.pick::<Yes>(()).unpack().is_yes(); +@@@ let prev = prev1; + let _confirm: bool = prev.pick::<bool>(["--confirm", "-C"]).unpack(); + ResultDone::default().to_render() +} +``` + +## 特殊用法:`usize` 解析 + +**Mingling** 为 `usize` 提供了一个特殊的用法:解析类似 `25G`、`32mib` 等字样 + +```rust +// Features: ["parser"] + +#[test] +fn parse_size() { + let vec = vec!["--size".to_string(), "25mib".to_string()]; + let size: usize = vec.pick(["--size", "-S"]).unpack(); + assert_eq!(size, 25 * 1024 * 1024); +} +``` + +## 自定义可解析类型 + +你可以使用 `Pickable` trait 使你的类型支持被 `Picker` 解析,这也是 `Picker` 拓展性的来源 + +```rust +// Features: ["parser"] +@@@use mingling::parser::{Pickable, Argument}; +@@@use mingling::Flag; +#[derive(Default, Clone)] +pub struct Address { + ip: String, + port: u16, +} + +impl Pickable for Address { + type Output = Self; + fn pick(args: &mut Argument, flag: Flag) -> Option<Self::Output> { + let raw = args.pick_argument(flag)?; + 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 }) + } +} +@@@dispatcher!("connect", CMDConnect => EntryConnect); +@@@pack!(ResultConnected = Address); + +#[chain] +fn handle_connect_entry(prev: EntryConnect) -> Next { + let address: Address = prev.pick("--addr").unpack(); + ResultConnected::new(address) +} + +#[renderer] +fn render_connected(addr: ResultConnected) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Connected: IP: {} PORT: {}", addr.ip, addr.port).ok(); + r +} +``` + +执行效果如下: + +```text +~# my-cli connect --addr 127.0.0.1:8080 +Connected: IP: 127.0.0.1 PORT: 8080 +``` + +## 自动为枚举实现 Pickable + +要为枚举类型实现 `Pickable`,只需该枚举实现了 `EnumTag`,然后为其实现 `PickableEnum` 即可 + +```rust +// Features: ["parser"] +@@@use mingling::parser::PickableEnum; +@@@use mingling::EnumTag; +#[derive(Debug, Default, EnumTag)] +pub enum Fruits { + #[default] + Apple, + Banana, + Orange, +} + +impl PickableEnum for Fruits {} +@@@dispatcher!("eat", CMDEat => EntryEat); +@@@pack!(ResultFruit = Fruits); + +#[chain] +fn handle_eat_entry(prev: EntryEat) -> Next { + let fruit: Fruits = prev.pick("--fruit").unpack(); + ResultFruit::new(fruit) +} + +#[renderer] +fn render_fruit(prev: ResultFruit) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Picked fruit: {:?}", *prev).ok(); + r +} +``` + +以上便是 `Picker` 的所有用法。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/_zh_CN/pages/7-argument-parse-clap.md b/docs/_zh_CN/pages/7-argument-parse-clap.md new file mode 100644 index 0000000..7dce301 --- /dev/null +++ b/docs/_zh_CN/pages/7-argument-parse-clap.md @@ -0,0 +1,92 @@ +<h1 align="center">使用 Clap 完成参数解析</h1> +<p align="center"> + 用 clap 做更复杂的参数解析 +</p> + +Picker 适合轻量级的参数提取,但当参数数量多、有复杂的校验规则、或者需要自动生成 `--help` 时,可以接入 [clap](https://crates.io/crates/clap)。 + +## 开启 clap 特性 + +```toml +[dependencies.mingling] +features = ["clap"] + +[dependencies.clap] +version = "4" +features = ["derive", "color"] +``` + +## dispatcher_clap + +`#[dispatcher_clap]` 加在 `clap::Parser` 结构体上,自动生成 Dispatcher: + +```rust +// Features: ["clap"] +// Dependencies: +// clap = "4" +@@@ use mingling::macros::dispatcher_clap; +#[derive(Default, clap::Parser, Groupped)] +#[dispatcher_clap("greet", CMDGreet, help = true, error = ErrorGreetParsed)] +pub struct EntryGreet { + #[clap(default_value = "World")] + name: String, + #[arg(short, long, default_value_t = 1)] + repeat: i32, +} + +#[renderer] +fn render_greet(greet: EntryGreet) -> RenderResult { + let mut r = RenderResult::new(); + let count = greet.repeat.max(0) as usize; + write!(r, "Hello, ").ok(); + for _ in 0..count { + write!(r, "{} ", greet.name).ok(); + } + writeln!(r, "!").ok(); + r +} + +#[renderer] +fn render_greet_parse_failed(err: ErrorGreetParsed) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "{}", *err).ok(); + r +} +``` + +## 与 BasicProgramSetup 配合 + +如果需要 `--help` 支持,在 main 中注册 `BasicProgramSetup` 并设置 clap 帮助的输出模式: + +```rust +// Features: ["clap"] +// Dependencies: +// clap = "4" +@@@use mingling::setup::BasicProgramSetup; +@@@use mingling::macros::dispatcher_clap; +@@@#[derive(Default, clap::Parser, Groupped)] +@@@#[dispatcher_clap("greet", CMDGreet)] +@@@pub struct EntryGreet { +@@@ name: String, +@@@} +@@@#[renderer] +@@@fn render_greet(greet: EntryGreet) -> RenderResult { +@@@ let mut r = RenderResult::new(); +@@@ write!(r, "Hello, {}!", greet.name).ok(); +@@@ r +@@@} +fn main() { + let mut program = ThisProgram::new(); + program.with_setup(BasicProgramSetup); + program.stdout_setting.clap_help_print_behaviour = + mingling::ClapHelpPrintBehaviour::WriteToRenderResult; + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} +``` + +详见 [example-clap-binding](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-clap-binding)。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/_zh_CN/pages/8-setup-and-resources.md b/docs/_zh_CN/pages/8-setup-and-resources.md new file mode 100644 index 0000000..d38e69d --- /dev/null +++ b/docs/_zh_CN/pages/8-setup-and-resources.md @@ -0,0 +1,89 @@ +<h1 align="center">程序装配</h1> +<p align="center"> + 用 Setup 初始化你的程序 +</p> + +当程序启动时需要做一些初始化工作——比如解析全局参数、注册资源——你可以用 `#[program_setup]` 来组织这些逻辑。 + +## 用 Setup 做初始化 + +```rust +// Features: ["extra_macros"] +@@@use mingling::macros::program_setup; +@@@use mingling::Program; +#[program_setup] +fn my_setup(program: &mut Program<ThisProgram>) { + // 从参数中提取全局标志 + program.global_flag(["-v", "--verbose"], |program| { + program.stdout_setting.verbose = true; + }); +} +@@@ +@@@fn main() { +@@@ let mut program = ThisProgram::new(); +@@@ program.with_setup(MySetup); +@@@ program.exec_and_exit(); +@@@} +@@@gen_program!(); +``` + +`#[program_setup]` 标记的函数接收 `&mut Program<ThisProgram>`,你可以在里面做任何初始化操作。 + +在 `main` 里通过 `program.with_setup(...)` 注册即可使用。 + +> [!NOTE] +> `#[program_setup]` 需要 `extra_macros` 特性。没有此特性时,可以手动实现 `ProgramSetup` trait。 + +## 提取全局参数 + +Setup 里最常用的操作就是提取全局参数。Mingling 提供了几个辅助方法: + +```rust +// Features: ["extra_macros"] +@@@use mingling::macros::program_setup; +@@@use mingling::Program; +#[program_setup] +fn my_setup(program: &mut Program<ThisProgram>) { + // 布尔标志 + program.global_flag(["-v", "--verbose"], |program| { + program.stdout_setting.verbose = true; + }); + + // 带值的参数 + program.global_argument("--name", |_program, value| { + // value 就是 "Alice" + let _ = value; + }); +} +``` + +> [!TIP] +> `global_flag` 和 `global_argument` 会自动从 `program.args` 中移除已匹配的参数,这些参数不会进入管线。 + +## 内置 Setup + +Mingling 提供了一些开箱即用的 Setup,覆盖了 CLI 程序最常见的需求: + +| Setup | 功能 | +| --------------------------- | --------------------------------------------------------------------- | +| `BasicProgramSetup` | 解析 `--help`/`-h`、`--quiet`/`-q`、`--confirm`/`-C` | +| `DirectoryEnvironmentSetup` | 注册目录资源:当前目录、可执行目录、Home 目录、临时目录 | +| `ExitCodeSetup` | 通过 `ResExitCode` 控制程序退出码 | +| `StructuralRendererSetup` | 启用 `--json`、`--yaml` 等结构化输出(需 `structural_renderer` 特性) | + +用法就是在 `main` 里加一行: + +```rust +@@@use mingling::setup::BasicProgramSetup; +fn main() { + let mut program = ThisProgram::new(); + program.with_setup(BasicProgramSetup); + program.exec_and_exit(); +} +``` + +`BasicProgramSetup` 帮你处理了绝大多数 CLI 程序都需要的通用参数,省去自己手动解析的麻烦。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/_zh_CN/pages/9-error-handling.md b/docs/_zh_CN/pages/9-error-handling.md new file mode 100644 index 0000000..07c82da --- /dev/null +++ b/docs/_zh_CN/pages/9-error-handling.md @@ -0,0 +1,128 @@ +<h1 align="center">错误处理</h1> +<p align="center"> + 将错误优雅地展示给用户 +</p> + +管线里不只有成功路径。当输入有误、资源找不到、操作失败时,你需要一个地方来处理这些"意外",而不是让程序 panic。 + +## 两个路径:成功 vs 错误 + +回顾管线模型,Chain 的返回值是 `Next`,它有两个去向: + +| 路由 | 含义 | +| -------------- | ---------------------------- | +| `.to_render()` | 出结果了,交给 Renderer 展示 | +| `.to_chain()` | 还没处理完,交给下一个 Chain | + +错误类型的值也可以走任意一条路——你可以选择直接渲染错误信息,也可以交给下一个 Chain 尝试恢复。 + +## 用独立类型区分错误 + +```rust +@@@dispatcher!("greet", CMDGreet => EntryGreet); +pack!(ResultGreeting = String); +pack!(ErrorNameEmpty = String); + +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + let name = args.inner.first().cloned().unwrap_or_default(); + + if name.is_empty() { + ErrorNameEmpty::new("name is required".to_string()).to_render() + } else { + ResultGreeting::new(name).to_render() + } +} +``` + +然后各自写 Renderer: + +```rust +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultGreeting = String); +@@@pack!(ErrorNameEmpty = String); +@@@#[chain] fn handle_greet(args: EntryGreet) -> Next { ResultGreeting::new(args.inner.first().cloned().unwrap_or_default()).to_render() } + +#[renderer] +fn render_greet(result: ResultGreeting) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Hello, {}!", *result).ok(); + r +} + +#[renderer] +fn render_error_name_empty(err: ErrorNameEmpty) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Error: {}", *err).ok(); + r +} +``` + +两个 Renderer 各司其职,用户看到什么取决于 Chain 返回了什么。 + +## 完整的例子 + +```rust +dispatcher!("greet", CMDGreet => EntryGreet); + +pack!(ResultGreeting = String); +pack!(ErrorNameEmpty = String); + +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + let name = args.inner.first().cloned().unwrap_or_default(); + if name.is_empty() { + ErrorNameEmpty::new("name is required".to_string()).to_render() + } else { + ResultGreeting::new(name).to_render() + } +} + +#[renderer] +fn render_greet(result: ResultGreeting) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Hello, {}!", *result).ok(); + r +} + +#[renderer] +fn render_error_name_empty(err: ErrorNameEmpty) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Error: {}", *err).ok(); + r +} + +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} + +gen_program!(); +``` + +运行效果: + +```text +~# my-cli greet Alice +Hello, Alice! + +~# my-cli greet +Error: name is required +``` + +## 关于 `pack_err!` + +如果你启用了 `extra_macros`,还可以用 `pack_err!` 快速声明带有自动 `name` 字段的错误类型: + +```rust +// Features: ["extra_macros"] +pack_err!(ErrorNotFound); +// 生成: struct ErrorNotFound { pub name: String } +``` + +详见 [特性列表](pages/other/features)。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/_zh_CN/pages/advanced/.name b/docs/_zh_CN/pages/advanced/.name new file mode 100644 index 0000000..eb92a2d --- /dev/null +++ b/docs/_zh_CN/pages/advanced/.name @@ -0,0 +1 @@ +进阶 diff --git a/docs/_zh_CN/pages/advanced/1-completion.md b/docs/_zh_CN/pages/advanced/1-completion.md new file mode 100644 index 0000000..3941404 --- /dev/null +++ b/docs/_zh_CN/pages/advanced/1-completion.md @@ -0,0 +1,83 @@ +<h1 align="center">补全</h1> +<p align="center"> + 使用 comp 特性实现完全动态的补全系统 +</p> + +Mingling 的补全是**完全动态**的——没有静态的补全文件,而是在运行时根据用户当前输入实时计算补全建议。 + +## 开启 comp + +```toml +# Cargo.toml +[dependencies.mingling] +features = ["comp"] + +[build-dependencies.mingling] +features = [ + "comp", + # 启用 `builds` 特性以提供构建期支持 + "builds" +] +``` + +## 工作原理 + +当用户按下 `TAB` 时,补全脚本会调用程序的隐藏子命令 `__comp`,它会根据输入的 `ShellContext` 动态地查询最合适的建议。 + +这个隐藏子命令由 `gen_program!()` 在启用 `comp` 特性时自动生成,对应的分发器是 `CMDCompletion`,你需要使用 `with_dispatcher` 添加到程序中。 + +补全流程: + +1. 二次匹配用户当前输入的 `Dispatcher` +2. 调用对应的 `#[completion]` 函数 +3. 函数返回 `Suggest`(文件补全或建议列表) +4. 通知 Shell 将建议呈现出来 + +## 定义补全 + +用 `#[completion(EntryType)]` 为 Entry 定义补全逻辑: + +```rust +// Features: ["comp"] +@@@use mingling::prelude::*; +@@@use mingling::{ShellContext, Suggest, SuggestItem}; +@@@use std::collections::BTreeSet; +@@@dispatcher!("greet", CMDGreet => EntryGreet); + +#[completion(EntryGreet)] +fn complete_greet(ctx: &ShellContext) -> Suggest { + if ctx.previous_word == "greet" { + let mut items = BTreeSet::new(); + items.insert(SuggestItem::new_with_desc("Alice".into(), "Likes to receive messages".into())); + items.insert(SuggestItem::new("World".into())); + Suggest::Suggest(items) + } else { + Suggest::FileCompletion + } +} +``` + +`suggest!` 宏是更简洁的写法,效果相同: + +```rust +// Features: ["comp"] +@@@use mingling::macros::suggest; +@@@fn example() { +suggest! { + "Alice": "Likes to receive messages", + "World" +}; +@@@} +``` + +`ShellContext` 包含用户当前的输入状态(`previous_word`、`current_word`、`all_words` 等),`Suggest` 有两种变体:`Suggest::Suggest(list)` 返回建议列表,`Suggest::FileCompletion` 交给 shell 自己做文件补全。 + +## 生成补全脚本 + +在 `build.rs` 中调用 `build_comp_scripts` 生成补全脚本(需要 `builds` + `comp` 特性)。 + +详见 [example-completion](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-completion)。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/_zh_CN/pages/advanced/2-structural-renderer.md b/docs/_zh_CN/pages/advanced/2-structural-renderer.md new file mode 100644 index 0000000..9a4f111 --- /dev/null +++ b/docs/_zh_CN/pages/advanced/2-structural-renderer.md @@ -0,0 +1,124 @@ +<h1 align="center">结构化渲染</h1> +<p align="center"> + 使用 structural_renderer 特性将结果渲染为序列化文本 +</p> + +启用 `structural_renderer` 后,你的程序可以通过 `--json`、`--yaml` 等参数将输出切换为结构化格式,方便与其他工具集成。 + +## 开启特性 + +```toml +[dependencies.mingling] +features = ["structural_renderer"] +``` + +`structural_renderer` 会自动启用 `json_serde_fmt`。 + +如果需要更多格式,可以启用 `structural_renderer_full`(包含 JSON、YAML、TOML、RON)。 + +> [!NOTE] +> 若需要定制输出类型,可以查看 [特性](./pages/other/features) + +## 基本用法 + +启用 `StructuralRendererSetup` 后,用 `pack_structural!` 替代 `pack!` 来声明支持结构化输出的类型: + +```rust +// Features: ["structural_renderer"] +// Dependencies: +// serde = "1" +@@@use mingling::setup::StructuralRendererSetup; +@@@dispatcher!("render", CMDRender => EntryRender); + +// pack_structural! 等价于 pack! + StructuralData +pack_structural!(ResultInfo = (String, i32)); + +#[chain] +fn handle_render(args: EntryRender) -> Next { + let name = args.inner.first().cloned().unwrap_or_default(); + let age = args.inner.get(1).and_then(|s| s.parse().ok()).unwrap_or(0); + ResultInfo::new((name, age)) +} + +#[renderer] +fn render_info(r: ResultInfo) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "{:?}", *r).ok(); + result +} +``` + +运行效果: + +```text +~# my-cli render Bob 22 +("Bob", 22) + +~# my-cli render Bob 22 --json +{"inner":["Bob",22]} +``` + +用户传入 `--json` 时,框架自动将渲染结果序列化为 JSON,无需修改业务代码。 + +## 自定义输出结构 + +`pack_structural!` 的默认输出包含 `inner` 字段。要完全控制输出结构,可以用 `#[derive(StructuralData, Serialize, Groupped)]` 手动定义类型: + +```rust +// Features: ["structural_renderer"] +// Dependencies: +// serde = "1" +@@@use mingling::prelude::*; +@@@use mingling::setup::StructuralRendererSetup; +@@@use mingling::StructuralData; +@@@use serde::Serialize; +@@@dispatcher!("render", CMDRender => EntryRender); + +#[derive(Serialize, StructuralData, Groupped)] +struct Info { + name: String, + age: i32, +} + +#[chain] +fn handle_render(args: EntryRender) -> Next { + let name = args.inner.first().cloned().unwrap_or_default(); + let age = args.inner.get(1).and_then(|s| s.parse().ok()).unwrap_or(0); + Info { name, age }.to_render() +} + +#[renderer] +fn render_info(info: Info) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "{} is {} years old", info.name, info.age).ok(); + r +} +@@@ +@@@fn main() { +@@@ let mut program = ThisProgram::new(); +@@@ program.with_setup(StructuralRendererSetup); +@@@ program.with_dispatcher(CMDRender); +@@@ program.exec(); +@@@} +@@@gen_program!(); +``` + +这时 `--json` 输出: + +```json +{ "name": "Bob", "age": 22 } +``` + +## 注意事项 + +- 支持格式:JSON、YAML、TOML、RON(取决于启用的特性) +- `StructuralRendererSetup` 注册 `--json`、`--yaml`、`--toml`、`--ron` 等全局参数 + +> [!NOTE] +> 每个类型仍需一个**空的 Renderer**,否则该类型 **不被视为可渲染** + +详见 [example-structural-renderer](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-structural-renderer)。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/_zh_CN/pages/concepts/.name b/docs/_zh_CN/pages/concepts/.name new file mode 100644 index 0000000..dfd4543 --- /dev/null +++ b/docs/_zh_CN/pages/concepts/.name @@ -0,0 +1 @@ +核心概念 diff --git a/docs/_zh_CN/pages/concepts/1-the-pipeline.md b/docs/_zh_CN/pages/concepts/1-the-pipeline.md new file mode 100644 index 0000000..b0bb19d --- /dev/null +++ b/docs/_zh_CN/pages/concepts/1-the-pipeline.md @@ -0,0 +1,129 @@ +<h1 align="center">基础管线</h1> +<p align="center"> + Mingling 的核心执行流程 +</p> + +Mingling 把命令的处理拆成三个独立的阶段:Dispatcher → Chain → Renderer。这篇讲实际的执行逻辑——从用户输入到最终输出,每一步发生了什么。 + +## 完整流程 + +```mermaid +graph TD + A["program.exec_and_exit()"] --> B["Hook: pre_dispatch"] + B --> C["Dispatch<br/>匹配命令 → Entry"] + C --> D["Hook: post_dispatch"] + D --> E{"user_context.help?"} + E -->|"true"| F["render_help<br/>直接跳帮助渲染"] + E -->|"false"| G{"has_chain?"} + G -->|"有"| H["Hook: pre_chain"] + H --> I["do_chain<br/>执行业务逻辑"] + I --> J{"ChainProcess?"} + J -->|"Ok(any, Renderer)"| K["Hook: pre_render →<br/>render → post_render"] + J -->|"Ok(any, Chain)"| G + J -->|"Err"| L["finish"] + G -->|"无"| M{"has_renderer?"} + M -->|"有"| K + M -->|"无"| N["build_renderer_not_found"] + N --> G + K --> O["Hook: finish → 返回 RenderResult"] + L --> O + F --> O +``` + +## 阶段详解 + +### 1. 分发 + +`exec_with_args` 首先调用 `dispatch_args_dynamic` 或 `dispatch_args_trie`(取决于是否启用 `dispatch_tree` 特性),将用户输入的参数与注册的 Dispatcher 匹配。 + +匹配规则是按空格分割的**前缀匹配**——优先匹配最长的那个。例如注册了 `remote.add` 和 `remote`,输入 `remote add origin` 会匹配 `remote.add`。 + +```mermaid +graph LR + Input["用户输入"] --> M{匹配 Dispatcher} + M -->|"匹配到"| E["调用 dispatcher.begin(args)<br/>返回包装好的 Entry"] + M -->|"未匹配"| NF["build_dispatcher_not_found<br/>生成 ErrorDispatcherNotFound"] +``` + +匹配成功后调用 `dispatcher.begin(args)`,返回 `ChainProcess::Ok((AnyOutput, _))`,即包装好用户输入参数的 Entry 类型。 + +如果没有匹配到任何 Dispatcher,则生成 `ErrorDispatcherNotFound`(包裹完整的输入参数),后续可以被 Renderer 处理显示 "Command not found"。 + +### 2. Help 短路 + +在进入主循环前,检查 `program.user_context.help`。如果为 `true`(由 `BasicProgramSetup` 中的 `HelpFlagSetup` 在解析到 `--help` 时设置),直接调用 `render_help` 跳过整个管线。 + +### 3. Chain 主循环 + +这是核心调度逻辑。每次循环检查当前的 `AnyOutput`: + +1. **有 Chain 能处理** → 执行 `C::do_chain(current)` + - 返回 `(AnyOutput, Renderer)` → 退出循环,进入渲染 + - 返回 `(AnyOutput, Chain)` → 继续循环,把结果交给下一个 Chain + - 返回 `Err` → 终止程序 + +2. **没有 Chain,但有 Renderer** → 直接渲染 + +3. **两者都没有** → 生成 `renderer_not_found`,再循环一次(因为刚生成的这个类型可能有 Renderer) + +```mermaid +graph TD + Start["当前 AnyOutput"] --> C{"has_chain?"} + C -->|"是"| Chain["do_chain"] + Chain -->|"返回 (any, Chain)"| C + Chain -->|"返回 (any, Renderer)"| Render["render"] + Chain -->|"Err"| Exit["退出"] + C -->|"否"| R{"has_renderer?"} + R -->|"是"| Render + R -->|"否"| N["build_renderer_not_found<br/>再试一次"] + N --> C +``` + +### 4. Render + +渲染阶段调用 `C::render(any, &mut render_result)`,根据 `member_id` 找到对应的 `#[renderer]` 函数,将结果写入 `RenderResult`。如果启用了 `structural_renderer`,还会根据 `program.structural_renderer_name` 将结果序列化为 JSON/YAML 等格式。 + +### 5. 退出 + +设置 `exit_code`,触发 `finish` hook,返回 `RenderResult`。 + +> [!TIP] +> 这套运行时调度代码由 `gen_program!()` 生成的枚举和 `ProgramCollect` 实现来驱动。 +> +> 编译期只生成了类型到 Chain / Renderer / Help / Completion 的映射关系,实际的匹配和路由都是在运行时完成的。 + +## 这和直接函数调用的区别 + +这套管线可以避免你写出如下的代码: + +```rust +@@@ struct Config; +@@@ impl Config { fn read() -> Self { Config } } +@@@ fn main() { +@@@ let json = true; +// 读取配置 +let mut config = Config::read(); + +// 执行操作 +let Ok(result) = operation(&mut config) else { + panic!("错误处理"); +}; + +// 渲染结果 +if json { + print_json(); +} else { + println!("成功!"); +} +@@@ } +@@@ fn operation(config: &mut Config) -> Result<(),()> { Ok(()) } +@@@ fn print_json() {} +``` + +Mingling 的管线把 **命令匹配**、**业务逻辑**、**输出展示** 拆成三个独立的位置,每个位置只负责一件事。 + +更重要的是,管线经过 hook 和 `AnyOutput` 机制,允许横切关注点(日志、鉴权、退出码)无侵入地插入,不会污染你的业务代码。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/_zh_CN/pages/concepts/2-resource.md b/docs/_zh_CN/pages/concepts/2-resource.md new file mode 100644 index 0000000..1052254 --- /dev/null +++ b/docs/_zh_CN/pages/concepts/2-resource.md @@ -0,0 +1,60 @@ +<h1 align="center">资源系统</h1> +<p align="center"> + Mingling 如何管理全局状态 +</p> + +命令行程序经常需要共享一些全局的东西——配置文件、数据库连接、计数器、当前工作目录。 + +在普通 Rust 里你可能会用 `OnceCell` 或 `lazy_static`,在 Mingling 里有一套统一的机制:**资源系统**。 + +## 什么是资源? + +资源就是在多个 Chain 和 Renderer 之间共享的数据。 + +你只需要定义一个类型、注册到 Program,然后在函数签名里声明你需要它——剩下的注入和生命周期管理都由框架完成。 + +## 核心机制:ResourceMarker + +任何同时实现了 `Default + Clone` 的类型都可以自动成为资源。框架会为它实现 `ResourceMarker` trait,使其具备: + +- **`res_clone()`** —— 当多个 Chain 同时访问时,框架可以通过 clone 来避免锁竞争 +- **`res_default()`** —— 资源未注册时提供兜底值 + +如果你需要更精细的生命周期控制,可以使用 `LazyRes<T>`。它允许资源在第一次被访问时才初始化,并且可以在析构时执行回调(比如退出前保存状态到磁盘)。 + +## 为什么不用全局变量? + +传统做法的静态变量是隐式依赖 —— 你看函数签名根本不知道它用了什么全局状态。而 Mingling 的资源注入让 **依赖显式化**: + +- 函数需要什么资源,参数列表就写什么 +- `&T` 表示只读访问,`&mut T` 表示可修改 +- 调用者一眼就能看出这个函数的副作用 + +例如: + +```rust +@@@ use mingling::res::ResExitCode; +@@@ pack!(ErrorFileNotFound = ()); +#[chain] +fn handle_error_file_not_found( + error: ErrorFileNotFound, + ec: &mut ResExitCode // 通过签名可以看出副作用! +) { + ec.exit_code = 2; // 这里修改了退出码 +} +``` + +## 资源与 Setup 的关系 + +资源通常通过两个途径注册到 Program: + +1. **直接注册** —— 在 `main` 中调用 `program.with_resource(...)` +2. **通过 Setup** —— 使用 `DirectoryEnvironmentSetup` 等内置 Setup 批量注册(如 `ResCurrentDir`、`ResHomeDir`) + +Setup 是比资源更高层的抽象,一个 Setup 可以注册多个资源并做其他初始化工作。 + +详见教程中的 [程序装配](./pages/8-setup-and-resources) 一章。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/_zh_CN/pages/concepts/3-any-output.md b/docs/_zh_CN/pages/concepts/3-any-output.md new file mode 100644 index 0000000..9b820da --- /dev/null +++ b/docs/_zh_CN/pages/concepts/3-any-output.md @@ -0,0 +1,73 @@ +<h1 align="center">任意输出机制</h1> +<p align="center"> + 关于 AnyOutput 和 ChainProcess 的运作模式 +</p> + +Dispatcher → Chain → Renderer 三阶段之间传递的数据是什么? + +Chain 的输出可能是一个成功结果、一个错误、或者还需要继续交给下一个 Chain——这些类型各不相同,管线如何在编译期不知道具体类型的情况下,把它们送到正确的地方? + +## AnyOutput:类型擦除 + 组标签 + +Mingling 的解法是把**所有类型擦除到同一个包装里**,然后用一个**枚举标签**来区分它们: + +``` +AnyOutput<G> +├── inner: Box<dyn Any + Send> ← 真正的数据,类型已被擦除 +├── type_id: TypeId ← 运行时类型 ID,用于安全 downcast +└── member_id: G ← 枚举标签,标记"这是谁" +``` + +这里的 `G` 就是 `gen_program!()` 生成的程序枚举(也就是你熟知的 `ThisProgram`)。 + +每个被 `pack!` 或 `#[derive(Groupped)]` 标记的类型都被分配到这个枚举的一个变体。 + +## ChainProcess:数据 + 路由 + +在 `AnyOutput` 的基础上,`ChainProcess<G>` 加了一个**路由信息**: + +``` +ChainProcess<G> +├── Ok(AnyOutput<G>, NextProcess) ← 携带数据,告诉调度器下一步去哪 +│ ├── NextProcess::Chain ← "还没完,继续交给下一个 Chain" +│ └── NextProcess::Renderer ← "出结果了,展示给用户" +└── Err(ChainProcessError) ← "出错了,终止程序" +``` + +这就是为什么 Chain 函数返回的不是裸数据,而是 `ChainProcess`——它把 **"下一步去哪"** 和 **"数据"** 打包在一起。 + +调度器根据 `NextProcess` 决定是继续循环还是退出渲染。 + +## Groupped:谁是谁 + +调度器如何知道 `AnyOutput` 里装的是 `ResultName` 还是 `ErrorUserBlocked`?答案是 `Groupped` trait: + +``` +trait Groupped<G> { + fn member_id() -> G; +} +``` + +当你用 `pack!(ResultName = String)` 时,宏自动为 `ResultName` 实现 `Groupped`,`member_id()` 返回枚举中对应的变体。调度器一看 `member_id`,就去找对应的 Chain 或 Renderer。 + +`to_chain()` 和 `to_render()` 本质上是 `AnyOutput` 的快捷方法,分别构造 `ChainProcess::Ok(any, Chain)` 和 `ChainProcess::Ok(any, Renderer)`。 + +## 调度的执行 + +在运行时,主循环的工作就是: + +1. 看当前 `AnyOutput` 的 `member_id` +2. 查这个变体有没有对应的 Chain → 有就执行,拿到新的 `AnyOutput` 和 `NextProcess` +3. 如果 `NextProcess` 是 `Chain` → 回到第 1 步 +4. 如果 `NextProcess` 是 `Renderer` → 退出循环,渲染 + +这套机制保证了**类型安全**:`gen_program!()` 生成的调度代码在做 `restore`(从 `Box<dyn Any>` 还原为具体类型)时,一定是在匹配的 `member_id` 分支内做的,不可能把 `ResultName` 的数据当作 `ErrorUserBlocked` 来解包。 + +> [!TIP] +> 日常开发中你不需要手动操作 `AnyOutput` 或 `ChainProcess`。 +> +> `pack!`、`#[chain]`、`#[renderer]` 这些宏帮你处理了所有的包装和解包。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/_zh_CN/pages/concepts/4-program-collect.md b/docs/_zh_CN/pages/concepts/4-program-collect.md new file mode 100644 index 0000000..f5e6b8f --- /dev/null +++ b/docs/_zh_CN/pages/concepts/4-program-collect.md @@ -0,0 +1,52 @@ +<h1 align="center">关于 ProgramCollect</h1> +<p align="center"> + 了解 gen_program!() 是如何生成程序的 +</p> + +每个 Mingling 程序最后都有一行 `gen_program!()`。它在背后做了三件事,把整个程序的骨架搭建出来。 + +## gen_program!() 的三件事 + +### 1. 生成枚举 + +扫描当前模块中所有 `pack!`、`#[chain]`、`#[renderer]` 等宏标记的类型,为每个类型生成一个枚举变体。 + +这个枚举就是 `AnyOutput<G>` 中 `G` 的类型 —— 调度器靠枚举变体来区分管线中传递的不同数据。 + +### 2. 生成 ProgramCollect 实现 + +`ProgramCollect` 是一个 trait,定义了 **"每个枚举变体对应什么类型、由谁处理"** 的映射关系: + +- **`do_chain`** —— 根据 `member_id` 调用对应的 `#[chain]` 函数,返回新的 `AnyOutput` 和 `NextProcess` +- **`render`** —— 根据 `member_id` 调用对应的 `#[renderer]` 函数,写入 `RenderResult` +- **`render_help`** —— 根据 `member_id` 调用对应的 `#[help]` 函数 +- **`has_chain` / `has_renderer`** —— 判断某个变体有没有对应的处理函数 +- **`build_dispatcher_not_found` / `build_renderer_not_found` / `build_empty_result`** —— 三个内置降级类型,处理边界情况 + +这套映射在运行时通过枚举匹配来完成——编译期只生成了枚举和匹配分支,实际的函数调用发生在运行时。 + +### 3. 生成 ThisProgram + +生成 `ThisProgram` 类型别名,指向 `Program<生成的枚举>`。这就是为什么在 `main` 中可以直接写 `ThisProgram::new()`——它就是你整个程序的完整类型。 + +--- + +## 关于 `pathf` 和 `dispatch_tree` 下的差异 + +以上是默认行为,但在启用特定 feature 时会有变化: + +### 1. `dispatch_tree` 特性 + +Dispatcher 的匹配不再使用 `Vec<Box<dyn Dispatcher>>` 做线性匹配,而是在编译期将子命令结构构建为前缀树(Trie)。 + +匹配复杂度从 `O(n)` 降到 `O(k)` —— `k` 是输入长度,与命令数量无关。 + +### 2. `pathf` 特性(Module Pathfinder) + +默认情况下所有宏标记的类型必须在同一模块才能被 `gen_program!()` 收集到 + +启用 `pathf` 后,编译期自动扫描所有子模块,找到所有用宏标记的类型并生成完整的模块路径引用 —— 类型定义在深层子模块也无需手动 `use`。 + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/_zh_CN/pages/other/naming_rule.md b/docs/_zh_CN/pages/other/naming_rule.md index 1f1ff84..5f2ac34 100644 --- a/docs/_zh_CN/pages/other/naming_rule.md +++ b/docs/_zh_CN/pages/other/naming_rule.md @@ -193,14 +193,18 @@ fn handle_state_operation_remotes(state: StateOperationRemotes, db: &ResDatabase // 结果渲染 #[renderer] -fn render_remote_added(result: ResultRemoteAdded) { - r_println!("Remote added: {}", result.inner); +fn render_remote_added(result: ResultRemoteAdded) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Remote added: {}", result.inner).ok(); + r } // 错误渲染 #[renderer] -fn render_error_repository_not_found(err: ErrorRepositoryNotFound) { - r_println!("Error: remote '{}' not found", err.inner); +fn render_error_repository_not_found(err: ErrorRepositoryNotFound) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Error: remote '{}' not found", err.inner).ok(); + r } ``` diff --git a/docs/css/dev-dark.css b/docs/css/dev-dark.css new file mode 100644 index 0000000..6410f6e --- /dev/null +++ b/docs/css/dev-dark.css @@ -0,0 +1,778 @@ +/* + * Mingling Dev Docs — Dark theme (Minimal / technical) + * Dark gray-blue, sans-serif body, crisp monospace code + */ + +* { + -webkit-font-smoothing: antialiased; + -webkit-overflow-scrolling: touch; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + -webkit-text-size-adjust: none; + -webkit-touch-callout: none; + box-sizing: border-box; +} +body:not(.ready) { + overflow: hidden; +} +body:not(.ready) [data-cloak], +body:not(.ready) .app-nav, +body:not(.ready) > nav { + display: none; +} +div#app { + font-size: 30px; + font-weight: lighter; + margin: 40vh auto; + text-align: center; +} +div#app:empty::before { + content: "Loading..."; +} +img.emoji { + height: 1.2em; + vertical-align: middle; +} +span.emoji { + font-family: + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", + "Noto Color Emoji"; + font-size: 1.2em; + vertical-align: middle; +} + +/* ── Progress bar ── */ +.progress { + background-color: #58a6ff; + height: 2px; + left: 0; + position: fixed; + right: 0; + top: 0; + transition: + width 0.2s, + opacity 0.4s; + width: 0%; + z-index: 999999; +} + +/* ── Search ── */ +.search a:hover { + color: #58a6ff; +} +.search .search-keyword { + color: #58a6ff; + font-style: normal; + font-weight: 600; +} + +/* ── Base ── */ +html, +body { + height: 100%; +} +body { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + background-color: #0d1117; + color: #e6edf3; + font-family: + system-ui, + -apple-system, + "Segoe UI", + "Noto Sans", + Helvetica, + Arial, + sans-serif; + font-size: 15px; + line-height: 1.7; + letter-spacing: 0; + margin: 0; + overflow-x: hidden; +} +img { + max-width: 100%; +} +a[disabled] { + cursor: not-allowed; + opacity: 0.6; +} +kbd { + border: solid 1px #30363d; + border-radius: 4px; + display: inline-block; + font-size: 12px !important; + line-height: 14px; + margin-bottom: 3px; + padding: 3px 6px; + vertical-align: middle; +} + +/* ── Nav bar (top) ── */ +.app-nav { + font-family: + system-ui, + -apple-system, + "Segoe UI", + "Noto Sans", + Helvetica, + Arial, + sans-serif; + margin: 0 0 0 auto; + position: fixed; + right: 30px; + top: 10px; + z-index: 10; +} +.app-nav a { + color: #8b949e; + font-size: 14px; + padding: 6px 14px; + text-decoration: none; + transition: color 0.15s; +} +.app-nav a:hover { + color: #58a6ff; +} +.app-nav a.active { + color: #58a6ff; +} + +/* ── Top nav bar (shared class with dev-doc.html) ── */ +.dev-nav a { + color: #8b949e; +} +.dev-nav a:hover { + color: #58a6ff; +} + +/* ── GitHub corner ── */ +.github-corner { + position: fixed; + right: 0; + top: 0; + z-index: 999; +} +.github-corner svg { + fill: #e6edf3; + height: 60px; + width: 60px; +} + +/* ── Main layout ── */ +main { + display: block; + height: 100%; + margin-left: 300px; + overflow-y: auto; + position: relative; + -webkit-overflow-scrolling: touch; +} + +/* ── Anchor links ── */ +.anchor { + display: block; + cursor: pointer; + text-decoration: none; + transition: all 0.15s; +} +.anchor span { + color: #e6edf3; +} +.anchor:hover { + text-decoration: underline; +} + +/* ── Sidebar ── */ +.sidebar { + background: #161b22; + border-right: 1px solid #30363d; + bottom: 0; + left: 0; + overflow-y: auto; + position: fixed; + top: 0; + width: 300px; + z-index: 1; + -webkit-overflow-scrolling: touch; +} +.sidebar > h1 { + border-bottom: 1px solid #30363d; + font-size: 18px; + font-weight: 600; + margin: 0; + padding: 16px 20px; + line-height: 1.4; +} +.sidebar > h1 a { + color: #e6edf3; + text-decoration: none; +} +.sidebar > h1 a:hover { + color: #58a6ff; +} +.sidebar .sidebar-nav { + padding: 12px 0 40px; +} +.sidebar .sidebar-nav > ul > li { + padding-left: 28px; +} +.sidebar .sidebar-nav > ul > li > ul { + padding-left: 8px; +} +.sidebar li.collapse .app-sub-sidebar { + display: none; +} +.sidebar ul { + list-style: none; + margin: 0; + padding: 0; +} +.sidebar li > p { + font-weight: 600; + margin: 0; +} +.sidebar ul, +.sidebar ul li { + list-style: none; +} +.sidebar ul li a { + border-right: 2px solid transparent; + color: #e6edf3; + display: block; + font-size: 14px; + line-height: 1.6; + padding: 6px 20px; + text-decoration: none; + transition: all 0.1s; +} +.sidebar ul li a:hover { + color: #58a6ff; + background: rgba(88, 166, 255, 0.08); +} +.sidebar ul li ul { + padding-left: 16px; +} +.sidebar ul li.active > a { + border-right-color: #58a6ff; + color: #58a6ff; + font-weight: 600; + background: rgba(88, 166, 255, 0.12); +} +.sidebar::-webkit-scrollbar { + width: 4px; +} +.sidebar::-webkit-scrollbar-thumb { + background: transparent; + border-radius: 2px; +} +.sidebar:hover::-webkit-scrollbar-thumb { + background: #30363d; +} +.sidebar:hover::-webkit-scrollbar-track { + background: transparent; +} + +/* ── Sidebar toggle ── */ +.sidebar-toggle { + background: transparent; + border: none; + bottom: 0; + cursor: pointer; + left: 0; + outline: none; + padding: 10px; + position: absolute; + text-align: center; + transition: opacity 0.3s; + width: 284px; + z-index: 10; + display: none; +} +.sidebar-toggle:hover .sidebar-toggle-button { + opacity: 0.6; +} +.sidebar-toggle span { + background: #58a6ff; + border-radius: 1px; + display: block; + height: 2px; + margin-bottom: 4px; + width: 16px; +} +body.sticky .sidebar, +body.sticky .sidebar-toggle { + position: fixed; +} + +/* ── Content area ── */ +.content { + display: block; + padding-bottom: 60px; + padding-top: 24px; +} +.markdown-section { + margin: 0 auto; + max-width: 880px; + padding: 24px 40px 40px; + position: relative; +} +.markdown-section > * { + box-sizing: border-box; +} +.markdown-section > :first-child { + margin-top: 0 !important; +} +.markdown-section hr { + border: none; + border-top: 1px solid #30363d; + margin: 32px 0; +} +.markdown-section table { + border-collapse: collapse; + border-spacing: 0; + display: block; + margin: 24px 0; + overflow: auto; + width: 100%; +} +.markdown-section th { + background: #161b22; + border: 1px solid #30363d; + font-weight: 600; + padding: 8px 12px; + text-align: left; +} +.markdown-section td { + border: 1px solid #30363d; + padding: 8px 12px; +} +.markdown-section tr:nth-child(2n) { + background: #161b22; +} + +/* ── Headings ── */ +.markdown-section h1, +.markdown-section h2, +.markdown-section h3, +.markdown-section h4, +.markdown-section strong { + color: #f0f6fc; + font-weight: 600; +} +.markdown-section h1 { + border-bottom: 1px solid #30363d; + font-size: 28px; + margin: 0 0 20px; + padding-bottom: 8px; +} +.markdown-section h2 { + border-bottom: 1px solid #30363d; + font-size: 22px; + margin: 28px 0 16px; + padding-bottom: 6px; +} +.markdown-section h3 { + font-size: 18px; + margin: 24px 0 12px; +} +.markdown-section h4 { + font-size: 16px; + margin: 20px 0 10px; +} +.markdown-section h5 { + font-size: 15px; + margin: 16px 0 8px; +} +.markdown-section h6 { + color: #8b949e; + font-size: 14px; + margin: 14px 0 8px; +} +.markdown-section a { + color: #58a6ff; + text-decoration: none; +} +.markdown-section a:hover { + text-decoration: underline; +} + +/* ── Inline code ── */ +.markdown-section code { + background: rgba(240, 246, 252, 0.08); + border-radius: 4px; + font-family: + "JetBrains Mono", "Fira Code", "Cascadia Code", "SF Mono", "Fira Mono", + "Source Code Pro", Menlo, Consolas, "DejaVu Sans Mono", monospace; + font-size: 13px; + margin: 0; + padding: 2px 6px; + word-break: break-word; +} + +/* ── Code blocks ── */ +.markdown-section pre { + background: #161b22; + border: 1px solid #30363d; + border-radius: 6px; + font-family: + "JetBrains Mono", "Fira Code", "Cascadia Code", "SF Mono", "Fira Mono", + "Source Code Pro", Menlo, Consolas, "DejaVu Sans Mono", monospace; + font-size: 13px; + line-height: 1.6; + margin: 16px 0; + overflow: auto; + padding: 16px; + position: relative; + word-wrap: normal; +} +.markdown-section pre > code { + background: transparent; + border: none; + border-radius: 0; + font-size: 13px; + margin: 0; + padding: 0; + white-space: pre; + word-break: normal; +} + +/* ── Blockquote ── */ +.markdown-section blockquote { + border-left: 4px solid #30363d; + color: #8b949e; + margin: 16px 0; + padding: 0 16px; +} + +/* ── Tip / warn callouts ── */ +.markdown-section p.tip { + background: rgba(88, 166, 255, 0.1); + border-left: 4px solid #58a6ff; + border-radius: 4px; + padding: 12px 16px; +} +.markdown-section p.tip:before { + color: #58a6ff; + content: "💡"; + font-weight: 600; + margin-right: 6px; +} +.markdown-section p.tip code { + background: rgba(88, 166, 255, 0.15); +} +.markdown-section p.warn { + background: rgba(210, 153, 34, 0.15); + border-left: 4px solid #d29922; + border-radius: 4px; + padding: 12px 16px; +} +.markdown-section ul.task-list > li { + list-style: none; +} + +/* ── Prism token overrides (dark) ── */ +.token.cdata, +.token.comment, +.token.doctype, +.token.prolog { + color: #8b949e; +} +.token.namespace { + opacity: 0.7; +} +.token.boolean, +.token.number { + color: #79c0ff; +} +.token.punctuation { + color: #e6edf3; +} +.token.property { + color: #79c0ff; +} +.token.tag { + color: #7ee787; +} +.token.string { + color: #a5d6ff; +} +.token.selector { + color: #d2a8ff; +} +.token.attr-name { + color: #79c0ff; +} +.language-css .token.string, +.style .token.string, +.token.entity, +.token.url { + color: #a5d6ff; +} +.token.attr-value, +.token.control, +.token.directive, +.token.unit { + color: #a5d6ff; +} +.token.function { + color: #d2a8ff; +} +.token.keyword { + color: #ff7b72; +} +.token.atrule, +.token.regex, +.token.statement { + color: #a5d6ff; +} +.token.placeholder, +.token.variable { + color: #ffa657; +} +.token.deleted { + text-decoration: line-through; +} +.token.inserted { + text-decoration: none; +} +.token.italic { + font-style: italic; +} +.token.bold, +.token.important { + font-weight: 700; +} +.token.important { + color: #ff7b72; +} +.token.entity { + cursor: help; +} +code .token { + -moz-osx-font-smoothing: initial; + -webkit-font-smoothing: initial; + min-height: 24px; +} + +/* ── Flexible alerts ── */ +.alert.callout { + border-radius: 6px; +} +.alert.callout.note { + border: 1px solid #58a6ff; + background: rgba(88, 166, 255, 0.08); +} +.alert.callout.tip { + border: 1px solid #3fb950; + background: rgba(63, 185, 80, 0.08); +} +.alert.callout.important { + border: 1px solid #a371f7; + background: rgba(163, 113, 247, 0.08); +} +.alert.callout.warning { + border: 1px solid #d29922; + background: rgba(210, 153, 34, 0.08); +} +.alert.callout.attention { + border: 1px solid #f85149; + background: rgba(248, 81, 73, 0.08); +} +.alert.callout code { + background-color: transparent; +} + +/* ── Search panel ── */ +.search { + border-bottom: 1px solid #30363d; + padding: 12px 16px 0; +} +.search .input-wrap { + position: relative; +} +.search .input-wrap input[type="search"] { + background: #0d1117; + border: 1px solid #30363d; + border-radius: 6px; + color: #e6edf3; + font-family: + system-ui, + -apple-system, + "Segoe UI", + "Noto Sans", + Helvetica, + Arial, + sans-serif; + font-size: 14px; + outline: none; + padding: 6px 32px 6px 12px; + width: 100%; +} +.search .input-wrap input[type="search"]::placeholder { + color: #8b949e; +} +.search .input-wrap input[type="search"]:focus { + border-color: #58a6ff; + box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.15); +} +.search .input-wrap .clear-button { + background: transparent; + border: none; + color: #8b949e; + cursor: pointer; + font-size: 18px; + line-height: 1; + padding: 0 8px; + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); +} +.search .input-wrap .clear-button:hover { + color: #e6edf3; +} +.search .results-panel { + padding: 8px 0; +} +.search .results-panel a { + color: #e6edf3; + display: block; + font-size: 14px; + padding: 4px 16px; + text-decoration: none; +} +.search .results-panel a:hover { + color: #58a6ff; +} + +/* ── App name in sidebar header ── */ +.app-name-link { + color: #e6edf3; + font-size: 18px; + font-weight: 600; + text-decoration: none; +} + +/* ── Iframe ── */ +.markdown-section iframe { + border: 1px solid #30363d; + border-radius: 6px; +} + +/* ── Prev/Next nav ── */ +.page-nav { + border-top: 1px solid #30363d; + display: flex; + margin-top: 40px; + padding-top: 24px; +} +.page-nav a { + border: 1px solid #30363d; + border-radius: 6px; + color: #e6edf3; + display: inline-flex; + flex: 1 1 50%; + font-size: 14px; + gap: 8px; + max-width: 50%; + padding: 12px 20px; + text-decoration: none; + transition: border-color 0.15s; +} +.page-nav a:hover { + border-color: #58a6ff; +} +.page-nav .nav-prev { + justify-content: flex-start; + margin-right: 8px; +} +.page-nav .nav-next { + justify-content: flex-end; + margin-left: auto; +} +.page-nav a:only-child { + max-width: 50%; +} +.page-nav .nav-label { + font-size: 12px; + opacity: 0.6; +} +.page-nav .nav-title { + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* ── Responsive ── */ +@media screen and (max-width: 768px) { + .github-corner, + .sidebar-toggle, + .sidebar { + position: fixed; + } + .app-nav { + margin-top: 0; + } + .app-nav li ul { + background: #0d1117; + border: 1px solid #30363d; + } + main { + margin-left: 0; + overflow-y: auto; + } + .sidebar { + border-right: 1px solid #30363d; + left: -300px; + transition: left 0.25s ease; + } + .content { + padding-top: 20px; + } + .app-nav, + .github-corner { + opacity: 1; + transition: opacity 0.25s ease; + } + .sidebar-toggle { + background: #0d1117; + border: 1px solid #30363d; + border-radius: 0 4px 4px 0; + display: flex; + flex-direction: column; + left: 0; + padding: 8px; + position: fixed; + top: 10px; + width: auto; + z-index: 20; + } + body.close .sidebar { + left: 0; + } + body.close .sidebar-toggle { + left: 300px; + } + body.close .content { + opacity: 0.3; + pointer-events: none; + } + body.close .app-nav, + body.close .github-corner { + opacity: 0; + pointer-events: none; + } +} + +@media print { + .github-corner, + .sidebar-toggle, + .sidebar, + .app-nav { + display: none; + } + main { + margin-left: 0; + } +} diff --git a/docs/css/dev-light.css b/docs/css/dev-light.css new file mode 100644 index 0000000..d5cd75e --- /dev/null +++ b/docs/css/dev-light.css @@ -0,0 +1,771 @@ +/* + * Mingling Dev Docs — Light theme (Minimal / technical) + * Clean white, sans-serif body, crisp monospace code + */ + +* { + -webkit-font-smoothing: antialiased; + -webkit-overflow-scrolling: touch; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + -webkit-text-size-adjust: none; + -webkit-touch-callout: none; + box-sizing: border-box; +} +body:not(.ready) { + overflow: hidden; +} +body:not(.ready) [data-cloak], +body:not(.ready) .app-nav, +body:not(.ready) > nav { + display: none; +} +div#app { + font-size: 30px; + font-weight: lighter; + margin: 40vh auto; + text-align: center; +} +div#app:empty::before { + content: "Loading..."; +} +img.emoji { + height: 1.2em; + vertical-align: middle; +} +span.emoji { + font-family: + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", + "Noto Color Emoji"; + font-size: 1.2em; + vertical-align: middle; +} + +/* ── Progress bar ── */ +.progress { + background-color: #0969da; + height: 2px; + left: 0; + position: fixed; + right: 0; + top: 0; + transition: + width 0.2s, + opacity 0.4s; + width: 0%; + z-index: 999999; +} + +/* ── Search ── */ +.search a:hover { + color: #0969da; +} +.search .search-keyword { + color: #0969da; + font-style: normal; + font-weight: 600; +} + +/* ── Base ── */ +html, +body { + height: 100%; +} +body { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + background-color: #ffffff; + color: #24292f; + font-family: + system-ui, + -apple-system, + "Segoe UI", + "Noto Sans", + Helvetica, + Arial, + sans-serif; + font-size: 15px; + line-height: 1.7; + letter-spacing: 0; + margin: 0; + overflow-x: hidden; +} +img { + max-width: 100%; +} +a[disabled] { + cursor: not-allowed; + opacity: 0.6; +} +kbd { + border: solid 1px #d0d7de; + border-radius: 4px; + display: inline-block; + font-size: 12px !important; + line-height: 14px; + margin-bottom: 3px; + padding: 3px 6px; + vertical-align: middle; +} + +/* ── Nav bar (top) ── */ +.app-nav { + font-family: + system-ui, + -apple-system, + "Segoe UI", + "Noto Sans", + Helvetica, + Arial, + sans-serif; + margin: 0 0 0 auto; + position: fixed; + right: 30px; + top: 10px; + z-index: 10; +} +.app-nav a { + color: #656d76; + font-size: 14px; + padding: 6px 14px; + text-decoration: none; + transition: color 0.15s; +} +.app-nav a:hover { + color: #0969da; +} +.app-nav a.active { + color: #0969da; +} + +/* ── GitHub corner ── */ +.github-corner { + position: fixed; + right: 0; + top: 0; + z-index: 999; +} +.github-corner svg { + fill: #24292f; + height: 60px; + width: 60px; +} + +/* ── Main layout ── */ +main { + display: block; + height: 100%; + margin-left: 300px; + overflow-y: auto; + position: relative; + -webkit-overflow-scrolling: touch; +} + +/* ── Anchor links ── */ +.anchor { + display: block; + cursor: pointer; + text-decoration: none; + transition: all 0.15s; +} +.anchor span { + color: #24292f; +} +.anchor:hover { + text-decoration: underline; +} + +/* ── Sidebar ── */ +.sidebar { + background: #f6f8fa; + border-right: 1px solid #d0d7de; + bottom: 0; + left: 0; + overflow-y: auto; + position: fixed; + top: 0; + width: 300px; + z-index: 1; + -webkit-overflow-scrolling: touch; +} +.sidebar > h1 { + border-bottom: 1px solid #d0d7de; + font-size: 18px; + font-weight: 600; + margin: 0; + padding: 16px 20px; + line-height: 1.4; +} +.sidebar > h1 a { + color: #24292f; + text-decoration: none; +} +.sidebar > h1 a:hover { + color: #0969da; +} + +.sidebar .sidebar-nav { + padding: 12px 0 40px; +} +.sidebar .sidebar-nav > ul > li { + padding-left: 28px; +} +.sidebar .sidebar-nav > ul > li > ul { + padding-left: 8px; +} +.sidebar li.collapse .app-sub-sidebar { + display: none; +} +.sidebar ul { + list-style: none; + margin: 0; + padding: 0; +} +.sidebar li > p { + font-weight: 600; + margin: 0; +} +.sidebar ul, +.sidebar ul li { + list-style: none; +} +.sidebar ul li a { + border-right: 2px solid transparent; + color: #24292f; + display: block; + font-size: 14px; + line-height: 1.6; + padding: 6px 20px; + text-decoration: none; + transition: all 0.1s; +} +.sidebar ul li a:hover { + color: #0969da; + background: rgba(9, 105, 218, 0.06); +} +.sidebar ul li ul { + padding-left: 16px; +} +.sidebar ul li.active > a { + border-right-color: #0969da; + color: #0969da; + font-weight: 600; + background: rgba(9, 105, 218, 0.08); +} +.sidebar::-webkit-scrollbar { + width: 4px; +} +.sidebar::-webkit-scrollbar-thumb { + background: transparent; + border-radius: 2px; +} +.sidebar:hover::-webkit-scrollbar-thumb { + background: #d0d7de; +} +.sidebar:hover::-webkit-scrollbar-track { + background: transparent; +} + +/* ── Sidebar toggle ── */ +.sidebar-toggle { + background: transparent; + border: none; + bottom: 0; + cursor: pointer; + left: 0; + outline: none; + padding: 10px; + position: absolute; + text-align: center; + transition: opacity 0.3s; + width: 284px; + z-index: 10; + display: none; +} +.sidebar-toggle:hover .sidebar-toggle-button { + opacity: 0.6; +} +.sidebar-toggle span { + background: #0969da; + border-radius: 1px; + display: block; + height: 2px; + margin-bottom: 4px; + width: 16px; +} +body.sticky .sidebar, +body.sticky .sidebar-toggle { + position: fixed; +} + +/* ── Content area ── */ +.content { + display: block; + padding-bottom: 60px; + padding-top: 24px; +} +.markdown-section { + margin: 0 auto; + max-width: 880px; + padding: 24px 40px 40px; + position: relative; +} +.markdown-section > * { + box-sizing: border-box; +} +.markdown-section > :first-child { + margin-top: 0 !important; +} +.markdown-section hr { + border: none; + border-top: 1px solid #d0d7de; + margin: 32px 0; +} +.markdown-section table { + border-collapse: collapse; + border-spacing: 0; + display: block; + margin: 24px 0; + overflow: auto; + width: 100%; +} +.markdown-section th { + background: #f6f8fa; + border: 1px solid #d0d7de; + font-weight: 600; + padding: 8px 12px; + text-align: left; +} +.markdown-section td { + border: 1px solid #d0d7de; + padding: 8px 12px; +} +.markdown-section tr:nth-child(2n) { + background: #f6f8fa; +} + +/* ── Headings ── */ +.markdown-section h1, +.markdown-section h2, +.markdown-section h3, +.markdown-section h4, +.markdown-section strong { + color: #1f2328; + font-weight: 600; +} +.markdown-section h1 { + border-bottom: 1px solid #d0d7de; + font-size: 28px; + margin: 0 0 20px; + padding-bottom: 8px; +} +.markdown-section h2 { + border-bottom: 1px solid #d0d7de; + font-size: 22px; + margin: 28px 0 16px; + padding-bottom: 6px; +} +.markdown-section h3 { + font-size: 18px; + margin: 24px 0 12px; +} +.markdown-section h4 { + font-size: 16px; + margin: 20px 0 10px; +} +.markdown-section h5 { + font-size: 15px; + margin: 16px 0 8px; +} +.markdown-section h6 { + color: #656d76; + font-size: 14px; + margin: 14px 0 8px; +} +.markdown-section a { + color: #0969da; + text-decoration: none; +} +.markdown-section a:hover { + text-decoration: underline; +} + +/* ── Inline code ── */ +.markdown-section code { + background: rgba(27, 31, 36, 0.06); + border-radius: 4px; + font-family: + "JetBrains Mono", "Fira Code", "Cascadia Code", "SF Mono", "Fira Mono", + "Source Code Pro", Menlo, Consolas, "DejaVu Sans Mono", monospace; + font-size: 13px; + margin: 0; + padding: 2px 6px; + word-break: break-word; +} + +/* ── Code blocks ── */ +.markdown-section pre { + background: #f6f8fa; + border: 1px solid #d0d7de; + border-radius: 6px; + font-family: + "JetBrains Mono", "Fira Code", "Cascadia Code", "SF Mono", "Fira Mono", + "Source Code Pro", Menlo, Consolas, "DejaVu Sans Mono", monospace; + font-size: 13px; + line-height: 1.6; + margin: 16px 0; + overflow: auto; + padding: 16px; + position: relative; + word-wrap: normal; +} +.markdown-section pre > code { + background: transparent; + border: none; + border-radius: 0; + font-size: 13px; + margin: 0; + padding: 0; + white-space: pre; + word-break: normal; +} + +/* ── Blockquote ── */ +.markdown-section blockquote { + border-left: 4px solid #d0d7de; + color: #656d76; + margin: 16px 0; + padding: 0 16px; +} + +/* ── Tip / warning callouts ── */ +.markdown-section p.tip { + background: rgba(9, 105, 218, 0.08); + border-left: 4px solid #0969da; + border-radius: 4px; + padding: 12px 16px; +} +.markdown-section p.tip:before { + color: #0969da; + content: "💡"; + font-weight: 600; + margin-right: 6px; +} +.markdown-section p.tip code { + background: rgba(9, 105, 218, 0.1); +} +.markdown-section p.warn { + background: rgba(242, 211, 91, 0.15); + border-left: 4px solid #d29922; + border-radius: 4px; + padding: 12px 16px; +} +.markdown-section ul.task-list > li { + list-style: none; +} + +/* ── Prism token overrides (light) ── */ +.token.cdata, +.token.comment, +.token.doctype, +.token.prolog { + color: #6e7781; +} +.token.namespace { + opacity: 0.7; +} +.token.boolean, +.token.number { + color: #0550ae; +} +.token.punctuation { + color: #24292f; +} +.token.property { + color: #0550ae; +} +.token.tag { + color: #116329; +} +.token.string { + color: #0a3069; +} +.token.selector { + color: #6f42c1; +} +.token.attr-name { + color: #0550ae; +} +.language-css .token.string, +.style .token.string, +.token.entity, +.token.url { + color: #0a3069; +} +.token.attr-value, +.token.control, +.token.directive, +.token.unit { + color: #0a3069; +} +.token.function { + color: #8250df; +} +.token.keyword { + color: #cf222e; +} +.token.atrule, +.token.regex, +.token.statement { + color: #0a3069; +} +.token.placeholder, +.token.variable { + color: #953800; +} +.token.deleted { + text-decoration: line-through; +} +.token.inserted { + text-decoration: none; +} +.token.italic { + font-style: italic; +} +.token.bold, +.token.important { + font-weight: 700; +} +.token.important { + color: #cf222e; +} +.token.entity { + cursor: help; +} +code .token { + -moz-osx-font-smoothing: initial; + -webkit-font-smoothing: initial; + min-height: 24px; +} + +/* ── Flexible alerts ── */ +.alert.callout { + border-radius: 6px; +} +.alert.callout.note { + border: 1px solid #0969da; + background: rgba(9, 105, 218, 0.06); +} +.alert.callout.tip { + border: 1px solid #1a7f37; + background: rgba(26, 127, 55, 0.06); +} +.alert.callout.important { + border: 1px solid #8250df; + background: rgba(130, 80, 223, 0.06); +} +.alert.callout.warning { + border: 1px solid #d29922; + background: rgba(210, 153, 34, 0.06); +} +.alert.callout.attention { + border: 1px solid #cf222e; + background: rgba(207, 34, 46, 0.06); +} +.alert.callout code { + background-color: transparent; +} + +/* ── Search panel ── */ +.search { + border-bottom: 1px solid #d0d7de; + padding: 12px 16px 0; +} +.search .input-wrap { + position: relative; +} +.search .input-wrap input[type="search"] { + background: #ffffff; + border: 1px solid #d0d7de; + border-radius: 6px; + color: #24292f; + font-family: + system-ui, + -apple-system, + "Segoe UI", + "Noto Sans", + Helvetica, + Arial, + sans-serif; + font-size: 14px; + outline: none; + padding: 6px 32px 6px 12px; + width: 100%; +} +.search .input-wrap input[type="search"]::placeholder { + color: #8b949e; +} +.search .input-wrap input[type="search"]:focus { + border-color: #0969da; + box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.15); +} +.search .input-wrap .clear-button { + background: transparent; + border: none; + color: #656d76; + cursor: pointer; + font-size: 18px; + line-height: 1; + padding: 0 8px; + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); +} +.search .input-wrap .clear-button:hover { + color: #24292f; +} +.search .results-panel { + padding: 8px 0; +} +.search .results-panel a { + color: #24292f; + display: block; + font-size: 14px; + padding: 4px 16px; + text-decoration: none; +} +.search .results-panel a:hover { + color: #0969da; +} + +/* ── App name in sidebar header ── */ +.app-name-link { + color: #24292f; + font-size: 18px; + font-weight: 600; + text-decoration: none; +} + +/* ── Iframe ── */ +.markdown-section iframe { + border: 1px solid #d0d7de; + border-radius: 6px; +} + +/* ── Prev/Next nav ── */ +.page-nav { + border-top: 1px solid #d0d7de; + display: flex; + margin-top: 40px; + padding-top: 24px; +} +.page-nav a { + border: 1px solid #d0d7de; + border-radius: 6px; + color: #24292f; + display: inline-flex; + flex: 1 1 50%; + font-size: 14px; + gap: 8px; + max-width: 50%; + padding: 12px 20px; + text-decoration: none; + transition: border-color 0.15s; +} +.page-nav a:hover { + border-color: #0969da; +} +.page-nav .nav-prev { + justify-content: flex-start; + margin-right: 8px; +} +.page-nav .nav-next { + justify-content: flex-end; + margin-left: auto; +} +.page-nav a:only-child { + max-width: 50%; +} +.page-nav .nav-label { + font-size: 12px; + opacity: 0.6; +} +.page-nav .nav-title { + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* ── Responsive ── */ +@media screen and (max-width: 768px) { + .github-corner, + .sidebar-toggle, + .sidebar { + position: fixed; + } + .app-nav { + margin-top: 0; + } + .app-nav li ul { + background: #ffffff; + border: 1px solid #d0d7de; + } + main { + margin-left: 0; + overflow-y: auto; + } + .sidebar { + border-right: 1px solid #d0d7de; + left: -300px; + transition: left 0.25s ease; + } + .content { + padding-top: 20px; + } + .app-nav, + .github-corner { + opacity: 1; + transition: opacity 0.25s ease; + } + .sidebar-toggle { + background: #ffffff; + border: 1px solid #d0d7de; + border-radius: 0 4px 4px 0; + display: flex; + flex-direction: column; + left: 0; + padding: 8px; + position: fixed; + top: 10px; + width: auto; + z-index: 20; + } + body.close .sidebar { + left: 0; + } + body.close .sidebar-toggle { + left: 300px; + } + body.close .content { + opacity: 0.3; + pointer-events: none; + } + body.close .app-nav, + body.close .github-corner { + opacity: 0; + pointer-events: none; + } +} + +@media print { + .github-corner, + .sidebar-toggle, + .sidebar, + .app-nav { + display: none; + } + main { + margin-left: 0; + } +} diff --git a/docs/dev/README.md b/docs/dev/README.md new file mode 100644 index 0000000..9289fcf --- /dev/null +++ b/docs/dev/README.md @@ -0,0 +1,21 @@ +<h1 align="center">Mingling Dev Docs</h1> + +<p align="center"> + Internal development documentation for the <b>Mingling</b> codebase — design notes, issue discussions, and architectural decisions. +</p> + +This site is separate from the [Helpdoc](https://mingling-rs.github.io/mingling/docs/doc.html). + +The helpdoc is user-facing: `tutorials`, `feature guides`, and `how-to content` for developers _using_ Mingling to build CLI applications. + +This dev-docs site is for developers **working on** Mingling itself — understanding internal mechanisms, tracking unresolved problems, and recording design rationale. + +## What's here + +- **Issues** — Collected notes on known pain points, unresolved trade-offs, and feature gaps in the current implementation. + +## How this is different from GitHub Issues + +Mingling is hosted on [GitHub](https://github.com/mingling-rs/mingling), and the [Issues page](https://github.com/mingling-rs/mingling/issues) there is primarily for discussion. + +In contrast, an issue in this document exists only when it is being planned or actively worked on. diff --git a/docs/dev/_sidebar.md b/docs/dev/_sidebar.md new file mode 100644 index 0000000..6b58d56 --- /dev/null +++ b/docs/dev/_sidebar.md @@ -0,0 +1,14 @@ +- [Welcome!](README) +* ❓ Issues + * [The Picker2 Arguments Parser](pages/issues/add-picker2) + * [Remove r_print! and r_println! Macros](pages/issues/remove-r-print-macro) + * [The Mod Pathfinder](pages/issues/the-mod-pathfinder) + * [Some Situations Where You'd Be Like "Shit!"](pages/issues/the-shit-time) +* 💡 Abouts + * [AI Translation Rule](pages/abouts/ai-translation-rule) + * [About Mingling CI Process](pages/abouts/ci) + * [Markdown Code Verification System](pages/abouts/code-verify-system) + * [About Nightly Features](pages/abouts/nightly-features) +* 📄 Templates + * [Changelog Template](pages/templates/changelog) + * [Helpdoc Template](pages/templates/doc) diff --git a/docs/dev/index.html b/docs/dev/index.html new file mode 100644 index 0000000..1bfb5c5 --- /dev/null +++ b/docs/dev/index.html @@ -0,0 +1,187 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta + name="viewport" + content="width=device-width, initial-scale=1, minimum-scale=1.0, shrink-to-fit=no, viewport-fit=cover" + /> + + <title>Mingling Dev Docs</title> + <meta + name="Mingling Dev Docs" + content="Internal design docs, issue discussions, and development notes for Mingling." + /> + + <link rel="preconnect" href="https://fonts.googleapis.com" /> + <link rel="icon" type="image/png" href="../res/favicon_small.png" /> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> + <link + href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" + rel="stylesheet" + /> + + <link rel="stylesheet" href="../css/dev-light.css" /> + <link + id="dark-style" + rel="stylesheet" + href="../css/dev-dark.css" + disabled + /> + + <style> + /* ── No sidebar header image override ── */ + .sidebar > h1 .app-name-link { + font-family: + "JetBrains Mono", "SF Mono", "Fira Code", "Cascadia Code", + monospace; + font-size: 16px; + font-weight: 600; + letter-spacing: -0.3px; + } + .sidebar > h1 .app-name-link::before { + content: "⚙ "; + font-family: + system-ui, + -apple-system, + sans-serif; + letter-spacing: 0; + } + .markdown-section h1, + .markdown-section h2, + .markdown-section h3, + .markdown-section h4 { + font-family: + "JetBrains Mono", "SF Mono", "Fira Code", "Cascadia Code", + monospace; + font-weight: 600; + letter-spacing: -0.5px; + } + .markdown-section h1 { + font-size: 24px; + } + .markdown-section h2 { + font-size: 19px; + } + .markdown-section h3 { + font-size: 16px; + } + .markdown-section p, + .markdown-section li { + font-family: + system-ui, + -apple-system, + "Segoe UI", + "Noto Sans", + Helvetica, + Arial, + sans-serif; + } + .sidebar ul li a { + font-family: + "JetBrains Mono", "SF Mono", "Fira Code", "Cascadia Code", + monospace; + font-size: 13px; + letter-spacing: -0.2px; + } + .sidebar ul li ul li a { + font-size: 12px; + } + /* ── Top nav bar ── */ + .dev-nav { + position: fixed; + top: 12px; + right: 30px; + z-index: 20; + display: flex; + gap: 4px; + align-items: center; + } + .dev-nav a { + font-family: + "JetBrains Mono", "SF Mono", "Fira Code", "Cascadia Code", + monospace; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: #656d76; + text-decoration: none; + padding: 4px 10px; + transition: color 0.15s; + } + .dev-nav a:hover { + color: #0969da; + } + </style> + </head> + + <body> + <nav class="dev-nav"> + <a + href="#" + onclick=" + toggleTheme(); + return false; + " + >🌓 Theme</a + > + <a href="../doc.html">📖 Helpdoc</a> + </nav> + + <div id="app"></div> + + <script> + window.$docsify = { + name: "dev-docs", + repo: "true", + corner: { + url: "https://github.com/mingling-rs/mingling", + icon: "github", + }, + auto2top: true, + loadSidebar: true, + maxLevel: 0, + subMaxLevel: 0, + search: { + placeholder: "Search dev docs…", + noData: "No matches found.", + depth: 2, + }, + plugins: [ + function (hook) { + hook.beforeEach(function (content) { + return content + .split("\n") + .filter(function (line) { + return !line.startsWith("@@@"); + }) + .join("\n"); + }); + }, + [ + "flexible-alerts", + { + style: "flat", + labels: { + NOTE: "Note", + TIP: "Tip", + IMPORTANT: "Important", + WARNING: "Warning", + CAUTION: "Caution", + }, + }, + ], + ], + }; + </script> + + <script src="../scripts/docsify.min.js"></script> + <script src="../scripts/search.js"></script> + <script src="../scripts/prism-bash.min.js"></script> + <script src="../scripts/prism-toml.min.js"></script> + <script src="../scripts/prism-rust.js"></script> + <script src="../scripts/docsify-corner.js"></script> + <script src="../scripts/docsify-plugin-flexible-alerts.js"></script> + <script src="../scripts/day-night-switch.js"></script> + </body> +</html> diff --git a/docs/dev/pages/abouts/.name b/docs/dev/pages/abouts/.name new file mode 100644 index 0000000..f2b936c --- /dev/null +++ b/docs/dev/pages/abouts/.name @@ -0,0 +1 @@ +💡 Abouts diff --git a/docs/dev/pages/abouts/ai-translation-rule.md b/docs/dev/pages/abouts/ai-translation-rule.md new file mode 100644 index 0000000..b1f93f6 --- /dev/null +++ b/docs/dev/pages/abouts/ai-translation-rule.md @@ -0,0 +1,116 @@ +<h1 align="center">AI Translation Rule</h1> +<p align="center"> + Translation prompt for your AI Agent +</p> + +# Translation Style Guide + +## 1. Tone & Voice + +### Preserve original tone + +Maintain the author's attitude, formality, and emotional register exactly as in the source. + +### Synonymous substitution + +Use words with close or equivalent meaning where direct translation is awkward or unnatural. + +## 2. Vocabulary & Abbreviation + +### Abbreviation + +Apply standard English abbreviations (e.g., _info_ for information, _dept_ for department) +to avoid overlong words, +but only when clarity is not sacrificed. + +### Concise expression + +Prefer shorter, more common alternatives (e.g., _use_ over _utilize_, _help_ over _facilitate_) +unless the original tone demands formality. + +## 3. Structural Rules + +### Paragraph integrity + +Keep the original paragraph breaks and line spacing. + +### Tag preservation + +Any inline Markdown formatting (bold, italic, code, links, lists) must be replicated exactly in translation. + +### Example + +- Before: “请保持专业语气,但避免使用过长的学术词汇。” +- After: “Keep a prof. tone, but avoid long academic words.” + +### Minimal diff + +When translating or syncing English content against a known Chinese original, +if the Chinese original's meaning is extremely close to the current English meaning, +do not modify the English text. +This is to keep git diffs friendly _(only modify parts that have truly changed)_. + +## 4. Exceptions + +- If a term has no common abbreviation, use the full word. +- If preserving tone requires a longer phrase, prioritize tone over brevity. + +## 5. Original Text + +```markdown +# Translation Style Guide + +## 1. Tone & Voice + +### Preserve original tone + +Maintain the author's attitude, formality, and emotional register exactly as in the source. + +### Synonymous substitution + +Use words with close or equivalent meaning where direct translation is awkward or unnatural. + +## 2. Vocabulary & Abbreviation + +### Abbreviation + +Apply standard English abbreviations (e.g., _info_ for information, _dept_ for department) +to avoid overlong words, +but only when clarity is not sacrificed. + +### Concise expression + +Prefer shorter, more common alternatives (e.g., _use_ over _utilize_, _help_ over _facilitate_) +unless the original tone demands formality. + +## 3. Structural Rules + +### Paragraph integrity + +Keep the original paragraph breaks and line spacing. + +### Tag preservation + +Any inline Markdown formatting (bold, italic, code, links, lists) must be replicated exactly in translation. + +### Example + +- Before: “请保持专业语气,但避免使用过长的学术词汇。” +- After: “Keep a prof. tone, but avoid long academic words.” + +### Minimal diff + +When translating or syncing English content against a known Chinese original, +if the Chinese original's meaning is extremely close to the current English meaning, +do not modify the English text. +This is to keep git diffs friendly _(only modify parts that have truly changed)_. + +## 4. Exceptions + +- If a term has no common abbreviation, use the full word. +- If preserving tone requires a longer phrase, prioritize tone over brevity. +``` + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/dev/pages/abouts/ci.md b/docs/dev/pages/abouts/ci.md new file mode 100644 index 0000000..3a93c1c --- /dev/null +++ b/docs/dev/pages/abouts/ci.md @@ -0,0 +1,71 @@ +<h1 align="center">About Mingling CI Process</h1> +<p align="center"> + CI workflow and local execution guide for Mingling +</p> + +Mingling's CI process is built into the project, with its execution logic located in `.run/src/bin/ci.rs`. You can run it locally via the `cargo ci` command, which produces the same results as the `CI` workflow in GitHub Actions. + +During development, you can run `cargo ci` at any time to verify that your code hasn't introduced regressions. + +## Running Locally + +An alias is defined in `.cargo/config.toml` at the project root: + +```toml +[alias] +ci = "run --manifest-path .run/Cargo.toml --bin ci --quiet --" +``` + +Simply execute: + +```bash +cargo ci +``` + +## CI Execution Flow + +`cargo ci` runs the following stages in order: + +### 1. Code Checking Stage (run by default, or individually via `--test-codes`) + +- **Scan and build all crates**: Recursively finds all `Cargo.toml` files in the project and runs `cargo build` for each crate in parallel. +- **Run Clippy on all crates**: Executes `cargo clippy ... -- -D warnings` in parallel; any warning will cause a failure. +- **Run unit tests for all crates**: Executes `cargo test` in parallel. + +### 2. Documentation and Example Checking Stage (run by default, or individually via `--test-docs`) + +- **Test all examples**: Runs the `test-examples` tool. +- **Verify Markdown code blocks compile**: Runs the `test-all-markdown-code` tool to check code blocks in all `*.md` files. See [ABOUT_CODE_VERIFY](docs/_ABOUT_CODE_VERIFY.md) for details. +- **Check if documentation is up to date**: Runs the following documentation refresh tools in sequence: + - `docs-code-box-fix` + - `docsify-sidebar-gen` + - `refresh-docs` + - `refresh-feature-mod` + - `sync-examples` +- Finally, runs `cargo fmt` to unify code formatting. + +### 3. File Normalization + +Runs `git add --renormalize .` to ensure file attributes such as line endings conform to the repository configuration. + +## Workspace Cleanliness and Temporary Commits + +To ensure reproducible CI results, `ci.rs` imposes strict requirements on the workspace state: + +- If the current workspace is not clean and `--dirty` has not been specified, the script will prompt whether to create a temporary commit: + - The commit message is `[DO NOT PUSH] CI TEMP [DO NOT PUSH]`. + - Use `-y` to auto-confirm without interaction. +- After CI finishes, the script automatically restores the workspace: + - First, `git reset --hard` discards all changes. + - If a temporary commit was created, it then runs `git reset --soft HEAD~1` and unstages everything, restoring the state to before CI started. +- If `--dirty` is specified, the temporary commit and the final cleanliness check are skipped. + +> **Warning**: `git reset --hard` is executed at the end of CI. If you use `--dirty`, ensure you have no unsaved important changes. + +## GitHub Actions Workflow + +`.github/workflows/ci.yml` defines the project's CI: + +- Triggered on `push` to the `main` branch. +- Runs `cargo ci` in parallel on `ubuntu-latest` and `windows-latest`. +- After CI passes, the `unreleased` tag is automatically moved to the latest commit on `main`. diff --git a/docs/_ABOUT_CODE_VERIFY.md b/docs/dev/pages/abouts/code-verify-system.md index 81c86fc..61b66e8 100644 --- a/docs/_ABOUT_CODE_VERIFY.md +++ b/docs/dev/pages/abouts/code-verify-system.md @@ -1,4 +1,7 @@ -# Doc Code Block Verification System +<h1 align="center">Markdown Code Verification System</h1> +<p align="center"> + A system that verifies every identified code block can be compiled +</p> This system automatically extracts and compiles Rust code blocks from docs, ensuring all example code stays usable in CI. diff --git a/ABOUT-NIGHTLY.md b/docs/dev/pages/abouts/nightly-features.md index f2d4ec7..13b666e 100644 --- a/ABOUT-NIGHTLY.md +++ b/docs/dev/pages/abouts/nightly-features.md @@ -1,4 +1,7 @@ -# About Nightly Rust +<h1 align="center">About Nightly Features</h1> +<p align="center"> + Using nightly Rust features in Mingling +</p> **Mingling** uses some features that are only available in the `nightly` toolchain. This requires you to enable the `nightly` feature: @@ -6,7 +9,7 @@ [dependencies] mingling = { version = "...", features = ["nightly"] } ``` - + ## Features > [!WARNING] diff --git a/docs/dev/pages/issues/.name b/docs/dev/pages/issues/.name new file mode 100644 index 0000000..b207fb4 --- /dev/null +++ b/docs/dev/pages/issues/.name @@ -0,0 +1 @@ +❓ Issues diff --git a/docs/dev/pages/issues/add-picker2.md b/docs/dev/pages/issues/add-picker2.md new file mode 100644 index 0000000..f5c3ca7 --- /dev/null +++ b/docs/dev/pages/issues/add-picker2.md @@ -0,0 +1,100 @@ +<h1 align="center">The Picker2 Arguments Parser</h1> +<p align="center"> + A smarter, faster alternative to Picker +</p> + +## Intro + +Mingling's `parser` feature is a temporary argument parsing solution created in the early stages of the project. While it can handle basic argument parsing tasks, its functionality is incomplete and has many limitations. + +This article aims to propose the design and development plan for the new Picker2 (feature name: `picker`). + +### Picker2 Expected Syntax + +1. **Pos args & flag args no longer depend on parsing order:** In `parser`, all flag args must be parsed before pos args. Picker2 removes this restriction. +2. **Declare flags with declarative macros:** Use `positional!()`, `flag![-X, --x]` etc. to declare flags — cleaner and more concise. +3. **One-shot `parse()` replaces step-by-step `unpack()`:** Defers the entire parsing flow to the final step, leaving more room for compile-time optimization. + +```rust +#[chain] +fn handle_hello(args: EntryHello) { + let parsed = args + .pick::<String>(positional!()) + .pick::<String>(flag![--help, -h]) + .parse(); +} +``` + +4. **Post-processing & routing control:** Use `after`, `after_route` to control post-processing logic. The route type is explicitly specified by the first `route` method. + +```rust +#[chain] +fn handle_hello(args: EntryHello) { + let parsed = args + .pick::<String>(positional!()) + .after(|v| format!("\"{}\"", v)) // post-processing + .parse(); +} +``` + +```rust +#[chain] +fn handle_hello(args: EntryHello) -> Next { + // requires impl Groupped<_> + // | + let parsed = args // vvvvvvvvvvvvvvvvvvv + .pick_route::<String, _>(positional!(), ErrorNoNameProvided) + .after_route(|v| Ok(format!("\"{}\"", v))) // post-process & route + .parse(); + let routed_parsed = route!(parsed); + empty_result!() +} +``` + +5. **Keep the `Pickable` Trait** +6. **Keep `PickableEnum` & provide a derive macro** +7. **Use `pick_optional`, `pick_vec` instead of implementing trait separately for `Option<T>`, `Vec<T>`** +8. **Use `pick_flag` instead of `pick::<bool>`** +9. **Provide `YesOrNo`, `TrueOrFalse` etc. that implement the `BoolFlag` trait for explicit bool extraction** +10. **Provide a `MultiFlag` derive macro, supporting arg modes like `pacman`** + +```rust +#[derive(MultiFlag)] +pub struct SomeToggles { + // default [-i] + pub install: bool, + + #[flag('U')] + pub uninstall: bool, + + #[flag('X')] + pub execute: bool, +} + +// Auto-implements Pickable, supports parsing args like `-iUX` +``` + +11. **Support multiple arg formats:** e.g. `--key=value`, `/Key=Value`, controlled by global config `PickerConfig` (or: `Picker::from_with_cfg(prev.inner, PickerConfig::default())`) + +12. `.parse()` no longer returns a bare tuple, but instead returns: + +```rust +pub struct PickerResult<Tuple> { + pub result: Tuple, + pub remains_argument: Arguments, +} +``` + +--- + +## 🕘 Progress + +- [x] In Progress + - [x] Added `Picker` struct and related call chain + - [x] Added `parselib` providing parsing logic + - [x] Added `Pickable` for extensibility + - [ ] Comprehensive testing! + - [ ] Improve documentation + - [ ] Add examples + - [ ] Update README +- [ ] Complete diff --git a/docs/dev/pages/issues/remove-r-print-macro.md b/docs/dev/pages/issues/remove-r-print-macro.md new file mode 100644 index 0000000..d6f6bff --- /dev/null +++ b/docs/dev/pages/issues/remove-r-print-macro.md @@ -0,0 +1,97 @@ +<h1 align="center">Remove r_print! and r_println! Macros</h1> + +`r_print!` and `r_println!` are important macros in Mingling for use inside `#[help]` and `#[renderer]` functions, but their implementation is not clean: they implicitly introduce a `__renderer_inner_result` field. While this might look elegant at the API level, it is **incorrect** and even **objectionable**. + +## Why **Objectionable**? + +Because you can't define declarative macros with `macro_rules` that wrap them. + +This is because `r_println!` depends on the implicit variable `__renderer_inner_result` injected by the `#[renderer]` proc macro into the function body. However, when a `macro_rules` declarative macro expands, **its internal code is placed in the caller's context**, which does not contain `__renderer_inner_result` — that variable only exists within the direct scope of the function body processed by `#[renderer]`. + +Let's look at some code to see why: + +```rust +// Suppose you want to write a wrapper macro: +macro_rules! my_println { + ($($arg:tt)*) => { + // When expanded here, the context is the call site of my_println!, + // not the location where the renderer function's injected variables live. + // So __renderer_inner_result is NOT visible here! + r_println!("Custom: {}", format!($($arg)*)); + }; +} + +#[renderer] +fn render_something(_p: ResultSomething) { + // Although this function body has __renderer_inner_result injected, + // the code from my_println! does NOT expand "inside this function body" — + // macro_rules expansion is essentially text replacement. The replaced code + // lives at the line where my_println! is called, and any variables referenced + // inside that macro must resolve to identifiers accessible at the call site. + // __renderer_inner_result is not a public, path-accessible variable; + // it's a hygienic local variable generated by the `#[renderer]` macro, + // and external macros cannot directly access it by name. + my_println!("{}", box_val); // Compile error: cannot find __renderer_inner_result +} +``` + +## Deeper Issues + +I have to admit, this is an early design flaw. After re-examining the code, I found the problem goes beyond "can't be wrapped". + +This isn't just a "can't wrap" issue — it reflects that `r_println!`'s design fundamentally violates Rust's macro hygiene principles: + +- **Implicit dependency**: Users of the macro must know that a variable named `__renderer_inner_result` exists — but this variable is neither part of the public API nor explicitly documented anywhere. +- **Scope leakage**: Variables injected by a proc macro should be confined to the scope processed by that macro. But `r_println!` attempts to make that variable accessible across macro calls, which effectively breaks Rust's identifier hygiene. +- **Non-composable**: Any attempt to wrap `r_println!` will fail, because declarative macros cannot "pass through" access to implicit variables. Even using a proc macro to wrap it would encounter similar hygiene issues. + +## Desired New Syntax + +I've designed two alternative approaches and will choose based on actual needs. + +### Option 1: Explicit Return + +```rust +#[renderer] +fn render_something(prev: ResultSomething) -> RenderResult { + let mut result = RenderResult::new(); + result.println(prev.to_string()); + // or + write!(result, "{}", prev.to_string()); + + result // return here +} +``` + +Clear boundaries — the entire rendering process is confined within the function body decorated by `#[help]` or `#[renderer]`, without introducing extra out-of-scope dependencies. The trade-off is slightly more boilerplate compared to the original approach. + +### Option 2: Resource Injection + +```rust +#[renderer] +fn render_something(prev: ResultSomething, result: &mut ResRenderResult) { + result.println(prev.to_string()); + // or + write!(result, "{}", prev.to_string()); + + result // return here +} +``` + +More flexible, but blurs the boundary between logic functions like `#[chain]` and rendering functions like `#[help]`. + +### Preferred Direction + +I lean toward **Option 1 (Explicit Return)**. There's no need to turn `RenderResult` into `ResRenderResult` as a global resource. + +As for rendering in logic functions like `#[chain]`, that should be handled by a separate system — not discussed here. + +## 🕘 Progress + +- [x] In Progress + - [x] Remove `r_println!` and `r_print!` macros + - [x] Modify `#[renderer]` and `#[help]` macros, remove implicit injection + - [x] Provide **no-return-value mode** and **RenderResult return value mode** for `#[renderer]` and `#[help]` macros + - [ ] Add new simplified syntax + - [x] Update documentation and test cases, ensure **all pass** +- [ ] Complete diff --git a/issues/the-mod-pathfinder.md b/docs/dev/pages/issues/the-mod-pathfinder.md index 5a0a4da..676251d 100644 --- a/issues/the-mod-pathfinder.md +++ b/docs/dev/pages/issues/the-mod-pathfinder.md @@ -1,4 +1,7 @@ -# The Mod Pathfinder +<h1 align="center">The Mod Pathfinder</h1> +<p align="center"> + A build-time analyzer that computes full module paths for Mingling types, resolving path ambiguity in macros. +</p> ## Background @@ -24,13 +27,13 @@ mod sub { mingling::macros::pack!(ResultMyName = String); // directly creates ..::sub::ResultMyName } ``` - + There are a few exceptions, such as the implicit Dispatcher provided by `extra_macros`, but these can be inferred from the node name: ```rust dispatcher!("remote.add"); // although the type is unknown, we can infer CMDRemoteAdd and EntryRemoteAdd ``` - + And also `#[program_setup]`: ```rust @@ -39,7 +42,7 @@ fn custom_setup(program: &mut Program<ThisProgram>) { program.with_dispatchers((CMD1, CMD2, CMD3, CMD4, CMD5)); } ``` - + ## Pathf Output Format Uses TOML key-value pairs, formatted as follows: @@ -47,13 +50,13 @@ Uses TOML key-value pairs, formatted as follows: ```toml ResultRemoteAdd = "crate::mymod::ResultRemoteAdd" ``` - + Recommended storage location is under the target directory: ``` /target/{target}/{crate-name}/type-mapping.toml ``` - + ## Other Issues This solution is limited to Mingling's own syntax system. If types like `dispatcher!`, `pack!` are indirectly expanded through macros, the analyzer will not be able to discover them. diff --git a/issues/the-shit-time.md b/docs/dev/pages/issues/the-shit-time.md index f830477..9d6c429 100644 --- a/issues/the-shit-time.md +++ b/docs/dev/pages/issues/the-shit-time.md @@ -1,4 +1,7 @@ -# Some Situations Where You'd Be Like "Shit!" +<h1 align="center">Some Situations Where You'd Be Like "Shit!"</h1> +<p align="center"> + This document collects the discomforts currently experienced while using Mingling. +</p> This document collects the discomforts currently experienced while using Mingling. @@ -20,7 +23,7 @@ completion: --help -h --- Display helps --version -V --- Display versions ``` - + Currently, there is no workaround. Ideal solution: @@ -31,7 +34,7 @@ fn complete(ctx: &ShellContext) -> Suggest { // ... } ``` - + --- ## Why can't I register descriptions for commands? @@ -47,7 +50,7 @@ mycmd <tab> completion: add rm list <--- You cannot register descriptions for commands ``` - + Expected behavior: ``` @@ -57,7 +60,7 @@ add --- Add something rm --- Remove something list --- List something ``` - + Ideal solution: ```rust @@ -67,11 +70,11 @@ dispatcher! { /// Add Something <--- How to i18n? "add", CMDAdd => EntryAdd } - + // Ideally, it should satisfy the following two conditions: // 1. No need to use `with_dispatcher`, because `with_dispatcher` is disabled in `dispatch_tree` mode // 2. Must be able to accept String or &str at runtime - + // Current idea #[inline(always)] #[dispatcher_desc(EntryAdd)] @@ -79,7 +82,17 @@ fn desc_add() -> String { // If using rust_i18n t!("cmd.add.desc") } - + +// Or + +#[completion(CMDAdd)] +fn desc_add(_ctx: &ShellContext) -> Suggest { + // If using rust_i18n + suggest{ + t!("cmd.add.desc") + } +} + // Collected and generated by `gen_program!()` // Generate something like get_dispatcher_desc(id: &ThisProgram) -> String // Match the corresponding function using enum values inside ThisProgram diff --git a/docs/dev/pages/templates/.name b/docs/dev/pages/templates/.name new file mode 100644 index 0000000..9e73889 --- /dev/null +++ b/docs/dev/pages/templates/.name @@ -0,0 +1 @@ +📄 Templates diff --git a/docs/dev/pages/templates/changelog.md b/docs/dev/pages/templates/changelog.md new file mode 100644 index 0000000..e416d52 --- /dev/null +++ b/docs/dev/pages/templates/changelog.md @@ -0,0 +1,28 @@ +<h1 align="center">Changelog Template</h1> +<p align="center"> + A template for writing changelog +</p> + +```markdown +### Unreleased + +#### Fixes: + +None + +#### Optimizations: + +None + +#### Features: + +None + +#### **BREAKING CHANGES** (API CHANGES): + +None +``` + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/_DOCS_TEMPLATE.md b/docs/dev/pages/templates/doc.md index 6a56bbd..e8a9308 100644 --- a/docs/_DOCS_TEMPLATE.md +++ b/docs/dev/pages/templates/doc.md @@ -1,15 +1,28 @@ +<h1 align="center">Helpdoc Template</h1> +<p align="center"> + A template for writing documentation +</p> + +When writing a Helpdoc, you can use the following template to draft + +```markdown <h1 align="center">Title</h1> <p align="center"> Description </p> - + Content here - + <!-- To display playable code if needed --> <!--<iframe src="../play/play.html?tur=default.md&title=Title" height="600px"/>--> - + <p align="center" style="font-size: 0.85em; color: gray;"> Written by @Your-Name </p> +``` + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/doc.html b/docs/doc.html index 783ccb4..a63fdd3 100644 --- a/docs/doc.html +++ b/docs/doc.html @@ -51,7 +51,7 @@ auto2top: true, loadSidebar: true, maxLevel: 0, - subMaxLevel: 3, + subMaxLevel: 0, search: { placeholder: "Search", noData: "No matches found.", diff --git a/docs/pages/1-getting-started.md b/docs/pages/1-getting-started.md index 5e746d3..2f31d69 100644 --- a/docs/pages/1-getting-started.md +++ b/docs/pages/1-getting-started.md @@ -13,19 +13,19 @@ Add the following to `Cargo.toml`: ```toml [dependencies.mingling] -version = "0.2.0" +version = "0.3.0" features = [] ``` ## Enable Features -**Mingling** has all features disabled by default and does not provide an all-in-one feature like `full`. +**Mingling** has all features disabled by default and does **not** provide an all-in-one feature like `full`. -Some features will **directly affect the behavior of the entire lifecycle**, so you need to enable them as needed, for example: +Some features **directly affect the entire lifecycle behavior**, so you need to enable them as needed, e.g.: ```toml [dependencies.mingling] -version = "0.2.0" +version = "0.3.0" features = [ "parser", "comp", @@ -33,11 +33,11 @@ features = [ ``` > [!NOTE] -> Visit [docs.rs](https://docs.rs/mingling/latest/mingling/feature/index.html) or [Features](pages/other/features) to learn about all available features. +> Visit [docs.rs](https://docs.rs/mingling/latest/mingling/feature/index.html) or [Features](pages/other/features) to learn about all features. ## Write the Basic Entry Point -Edit `src/main.rs` with the following code: +Write the following code in `src/main.rs`: ```rust use mingling::prelude::*; @@ -52,13 +52,13 @@ gen_program!(); ``` > [!IMPORTANT] -> Almost all Rust code blocks in the documentation have been compiled through the CI pipeline and are guaranteed to be usable. +> Almost all Rust code blocks in the docs have been compiled in CI and are guaranteed to work. > -> However, code blocks starting with `// NOT VERIFIED` have **not been verified**. +> However, code blocks starting with `// NOT VERIFIED` are **not verified**. > -> Want to know which `*.md` files have been compiled? Check [`verified-docs.toml`](https://github.com/mingling-rs/mingling/blob/main/verified-docs.toml) +> Want to know which `*.md` files are compiled? See [`verified-docs.toml`](https://github.com/mingling-rs/mingling/blob/main/verified-docs.toml). -## Verify Compilation +## Verify with Compilation ```plaintext ~# cargo check @@ -66,4 +66,4 @@ gen_program!(); --- -Once everything is good, start building! +Once everything is good, start writing something! diff --git a/docs/pages/10-help.md b/docs/pages/10-help.md new file mode 100644 index 0000000..c17f410 --- /dev/null +++ b/docs/pages/10-help.md @@ -0,0 +1,73 @@ +<h1 align="center">Help Info</h1> +<p align="center"> + Adding --help support to commands +</p> + +A CLI without help info is not a good CLI. + +In Mingling, use the `#[help]` macro to add help text to commands. + +## Simplest Help + +Write a help function directly for an Entry: + +```rust +@@@use mingling::macros::help; +@@@dispatcher!("greet", CMDGreet => EntryGreet); +#[help] +fn help_greet(entry: EntryGreet) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Usage: greet [name]").ok(); + writeln!(r, "Say hello to someone.").ok(); + r +} +``` + +> [!NOTE] +> Help functions also use `writeln!` into a `RenderResult`, because `#[help]` follows the rendering pipeline — it's a short-circuit render triggered early by the `--help` flag, not logic outside the pipeline. + +## Global Help + +You can also write help for `ErrorDispatcherNotFound` as the "root help": + +```rust +@@@use mingling::macros::help; +// Triggered when user passes --help directly +#[help] +fn help_root(entry: ErrorDispatcherNotFound) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Usage: my-cli <command>").ok(); + writeln!(r, "Commands:").ok(); + writeln!(r, " greet Say hello").ok(); + r +} +``` + +> [!TIP] +> `ErrorDispatcherNotFound` is a type generated by `gen_program!()`, representing "no matching command found." Writing `#[help]` for it adds help to the program's root command. + +## Requires Setup + +For `--help` to work properly, add `BasicProgramSetup` in `main`: + +```rust +@@@use mingling::macros::help; +@@@use mingling::setup::BasicProgramSetup; +@@@dispatcher!("greet", CMDGreet => EntryGreet); +fn main() { + let mut program = ThisProgram::new(); + program.with_setup(BasicProgramSetup); + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} +``` + +`BasicProgramSetup` includes `HelpFlagSetup`, which simply sets `program.user_context.help` to `true`. + +The actual routing to the `#[help]` function is handled by code generated via `gen_program!()` — it checks this flag during dispatch, and if `true`, goes through the help rendering path, bypassing the Chain. + +Without `BasicProgramSetup`, `--help` is treated as a normal argument and passed as input to the Entry's Chain. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/11-resource-system.md b/docs/pages/11-resource-system.md new file mode 100644 index 0000000..bc2197f --- /dev/null +++ b/docs/pages/11-resource-system.md @@ -0,0 +1,93 @@ +<h1 align="center">Using the Resource System</h1> +<p align="center"> + A hands-on guide to resources +</p> + +Resources are Mingling's mechanism for managing global state. Any type that implements `Default + Clone` can be a resource. + +## Define a Resource + +```rust +// Any type implementing Default + Clone can be used as a resource +#[derive(Default, Clone)] +struct ResCurrentDir(String); + +// Register with the Program +fn main() { + let mut program = ThisProgram::new(); + program.with_resource(ResCurrentDir(".".into())); + program.exec_and_exit(); +} +``` + +Since `ResCurrentDir` implements both `Default` and `Clone`, the framework automatically implements `ResourceMarker` for it — no manual impl needed. + +## Inject & Use + +In a Chain or Renderer, simply declare the resource in the parameter list: + +```rust +@@@#[derive(Default, Clone)] +@@@struct ResCurrentDir(String); +@@@dispatcher!("pwd", CMDPrintWorkingDir => EntryPrintWorkingDir); +@@@pack!(ResultPath = String); +// Inject read-only resource via &T +#[chain] +fn handle_pwd(_args: EntryPrintWorkingDir, cwd: &ResCurrentDir) -> Next { + ResultPath::new(cwd.0.clone()).to_render() +} + +#[renderer] +fn render_path(result: ResultPath) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "{}", *result).ok(); + r +} +``` + +## Modify a Resource + +Use `&mut T` to inject a mutable resource: + +```rust +@@@#[derive(Default, Clone)] +@@@struct ResVisitCount(u32); +@@@dispatcher!("visit", CMDVisit => EntryVisit); +@@@pack!(ResultDone = ()); +#[chain] +fn handle_visit(_args: EntryVisit, counter: &mut ResVisitCount) -> Next { + counter.0 += 1; + ResultDone::default() +} + +#[renderer] +fn render_done(_done: ResultDone, counter: &ResVisitCount) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "visit count is : {}", counter.0).ok(); + r +} +``` + +## Use Multiple Resources + +A Chain can inject any number of resources at once — the framework matches them by type automatically: + +```rust +@@@#[derive(Default, Clone)] struct ResConfig(String); +@@@#[derive(Default, Clone)] struct ResCounter(u32); +@@@dispatcher!("test", CMDTest => EntryTest); +@@@pack!(ResultDone = ()); +// Inject both read-only and mutable resources +#[chain] +fn handle_test(_args: EntryTest, config: &ResConfig, counter: &mut ResCounter) -> Next { + println!("config: {}", config.0); + counter.0 += 1; + ResultDone::default().to_render() +} +``` + +For deeper topics like `ResourceMarker`, lazy loading with `LazyRes`, etc., see [Core Concepts: Resource System](pages/concepts/2-resource). + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/12-exit-code.md b/docs/pages/12-exit-code.md new file mode 100644 index 0000000..6828cde --- /dev/null +++ b/docs/pages/12-exit-code.md @@ -0,0 +1,68 @@ +<h1 align="center">Exit Code Control</h1> +<p align="center"> + Managing program exit codes via the resource system +</p> + +Providing the shell with a correct exit code when a program terminates is a basic CLI convention. Mingling offers a ready-to-use `ExitCodeSetup` that, together with the `ResExitCode` resource, makes exit code control incredibly simple. + +## Enabling ExitCodeSetup + +```rust +@@@use mingling::prelude::*; +@@@use mingling::setup::ExitCodeSetup; +fn main() { + let mut program = ThisProgram::new(); + program.with_setup(ExitCodeSetup::default()); +@@@ program.exec_and_exit(); +} +``` + +`ExitCodeSetup` does two things: + +1. Registers the `ResExitCode` resource (default value `0`) +2. Registers a `finish` hook that reads the value of `ResExitCode` as the final exit code before the program exits + +## Modifying the Exit Code + +In a Chain or Renderer, inject `ResExitCode` to modify the exit code: + +```rust +@@@use mingling::res::ResExitCode; +@@@use mingling::setup::ExitCodeSetup; +@@@pack!(EntryCheck = Vec<String>); +#[chain] +fn handle_check(_args: EntryCheck, ec: &mut ResExitCode) { + // Modify exit code when check fails + ec.exit_code = 1; +} +``` + +> [!TIP] +> `ResExitCode` is simply `struct ResExitCode { pub exit_code: i32 }`. Inject `&mut ResExitCode` and modify the field directly. + +## Three Execution Modes of `Program` + +`Program` provides three execution modes (excluding `exec_repl` under the `repl` feature): + +| Mode | Behavior | +| ------------------------------- | ----------------------------------------------------------------------------------------- | +| `program.exec_and_exit()` | Executes and terminates the process directly with the exit code | +| `program.exec()` | Executes and returns an `i32` exit code, letting the caller decide handling | +| `program.exec_without_render()` | Returns `Result<RenderResult, ProgramExecuteError>`, with internal `exit_code` accessible | + +```rust +@@@use mingling::setup::ExitCodeSetup; +fn main() { + let mut program = ThisProgram::new(); + program.with_setup(ExitCodeSetup::default()); + + // Get exit code and handle it yourself + let exit_code = program.exec(); + std::process::exit(exit_code); +} +@@@gen_program!(); +``` + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/13-hook.md b/docs/pages/13-hook.md new file mode 100644 index 0000000..26f8712 --- /dev/null +++ b/docs/pages/13-hook.md @@ -0,0 +1,107 @@ +<h1 align="center">Hook System</h1> +<p align="center"> + How to insert custom behavior into a program using ProgramHook +</p> + +Hooks let you insert custom logic at various lifecycle points in the pipeline — before dispatch, after chain, before/after render, on program exit … + +You can write cross-cutting concerns (logging, auth, metrics collection) in hooks instead of scattering them across business code. + +## Basic Usage + +`ProgramHook` uses a builder pattern: + +```rust +@@@use mingling::hook::ProgramHook; +fn main() { + let mut program = ThisProgram::new(); + program.with_hook( + ProgramHook::empty() + .on_pre_chain(|info| { + println!("before chain: {}", info.input); + }) + .on_post_render(|info| { + println!("after render: {}", info.result); + }), + ); + program.exec_and_exit(); +} +``` + +> [!TIP] +> `ProgramHook::empty()` creates an empty hook, then chain-calls `.on_*()` methods to register the lifecycle nodes you care about. Unregistered nodes won't execute. + +## Lifecycle Nodes + +Hooks cover the full pipeline lifecycle: + +| Stage | Hook | Trigger Point | +| ------------ | ------------------ | ------------------- | +| **Dispatch** | `on_begin` | Execution start | +| | `on_pre_dispatch` | Before dispatch | +| | `on_post_dispatch` | After dispatch | +| **Chain** | `on_pre_chain` | Before chain exec | +| | `on_post_chain` | After chain exec | +| **Render** | `on_pre_render` | Before render exec | +| | `on_post_render` | After render exec | +| **Finish** | `on_finish` | Before program exit | + +Each hook callback receives a corresponding `Hook*Info` struct containing context info (input type, params, render results, etc.). + +## Real Example: Logging Operations + +```rust +@@@use mingling::prelude::*; +@@@use mingling::hook::ProgramHook; +@@@ +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +@@@ +@@@#[chain] fn handle_greet(args: EntryGreet) -> Next { +@@@ ResultName::new(args.inner.first().cloned().unwrap_or_default()).to_render() +@@@} +@@@#[renderer] fn render_name(r: ResultName) -> RenderResult { RenderResult::new() } +fn main() { + let mut program = ThisProgram::new(); + + // Log info before and after each chain execution + program.with_hook( + ProgramHook::empty() + .on_pre_chain(|info| { + eprintln!("[hook] executing chain for: {}", info.input); + }) + .on_post_chain(|info| { + eprintln!("[hook] chain output: {}", info.output.member_id); + }), + ); + + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} +``` + +Run output: + +```text +[hook] executing chain for: EntryGreet +[hook] chain output: ResultName +Hello, World! +``` + +## Controlling Behavior via Hooks + +Hooks aren't just for observation — you can use `ProgramControlUnit` to alter program behavior: + +| Variant | Effect | +| -------------------------- | ----------------------------------------- | +| `Continue` | Do nothing, continue execution | +| `OverrideExitCode(i32)` | Override the exit code | +| `RouteToChain(AnyOutput)` | Replace current data, re-enter Chain loop | +| `RouteToRender(AnyOutput)` | Skip subsequent Chain, render directly | + +> [!NOTE] +> Multiple hooks can be registered and execute in registration order. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/14-testing.md b/docs/pages/14-testing.md new file mode 100644 index 0000000..fa196b0 --- /dev/null +++ b/docs/pages/14-testing.md @@ -0,0 +1,130 @@ +<h1 align="center">Testing Your Program</h1> +<p align="center"> + Writing unit tests for Chain and Renderer +</p> + +A built-in benefit of the pipeline model is **testability**. + +A Chain is just a function that takes input and returns output; a Renderer is just a function that takes input and writes content — no global state magic, testing is straightforward. + +## Testing Renderer + +Renderer is the easiest to test — call the function, assert the result: + +```rust +@@@pack!(ResultName = String); +#[renderer] +fn render_greet(result: ResultName) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Hello, {}!", *result).ok(); + r +} + +#[test] +fn test_render_name() { + let result = render_name(ResultName::new("Alice".to_string())); + assert_eq!(result.to_string().as_str(), "Hello, Alice!\n"); +} +``` + +Note the Renderer's return type changed to `-> String` — `#[renderer]` auto-converts `RenderResult` to whatever return type you specify (default is `()`). By returning `String`, you can directly assert on the output content. + +## Testing Chain + +Testing a Chain is slightly more complex because its return value is `Next` (actually `impl Into<ChainProcess<ThisProgram>>`). You'll need the assertion macros provided by the framework: + +```rust +@@@use mingling::{assert_member_id, assert_render_result, unpack_chain_process}; +@@@dispatcher!("hello", CMDHello => EntryHello); +@@@pack!(ResultName = String); +@@@pack!(ErrorNoName = ()); +@@@#[chain] +@@@fn handle_hello(args: EntryHello) -> Next { +@@@ let name = args.inner.first().cloned().unwrap_or_default(); +@@@ if name.is_empty() { +@@@ ErrorNoName::default().to_render() +@@@ } else { +@@@ ResultName::new(name).to_render() +@@@ } +@@@} +#[test] +fn test_handle_hello_with_name() { + let chain_process = handle_hello(EntryGreet::new(vec!["Alice".to_string()])).into(); + // Asserts this is a render result (not continuing the chain) + assert_render_result!(chain_process); + // Asserts member_id is ResultName + assert_member_id!(chain_process, ResultName); + // Unpacks the inner value + let result_name = unpack_chain_process!(chain_process, ResultName); + assert_eq!(result_name.inner, "Alice"); +} +``` + +What the three test macros do: + +| Macro | Function | +| ----------------------- | ----------------------------------------------------------------- | +| `assert_render_result!` | Asserts Chain returned the render path (not continuing the chain) | +| `assert_member_id!` | Asserts the return value's member ID is a certain type | +| `unpack_chain_process!` | Unpacks the original type from ChainProcess | + +## Constructing Data with the entry! Macro + +If `extra_macros` is enabled, you can use `entry!` to quickly construct an Entry: + +```rust +// Features: ["extra_macros"] + +@@@use mingling::{assert_member_id, unpack_chain_process}; +@@@use mingling::macros::entry; +@@@dispatcher!("hello", CMDHello => EntryHello); +@@@pack!(ResultName = String); +@@@#[chain] +@@@fn handle_hello(args: EntryHello) -> Next { +@@@ let name = args.inner.first().cloned().unwrap_or_default(); +@@@ ResultName::new(name).to_render() +@@@} +#[test] +fn test_with_entry_macro() { + // entry! constructs an Entry from string literals + let entry = entry!("--name", "Alice"); + let chain_process = handle_hello(entry).into(); + let result_name = unpack_chain_process!(chain_process, ResultName); + assert_eq!(result_name.inner, "Alice"); +} +``` + +## Testing Resource Injection + +If a Chain uses resources, you need to provide resource instances in the test: + +```rust +@@@use mingling::{assert_render_result, unpack_chain_process}; +@@@#[derive(Default, Clone)] +@@@struct ResPrefix(String); +@@@dispatcher!("hello", CMDHello => EntryHello); +@@@pack!(ResultGreeting = String); +@@@ +#[chain] +fn handle_hello(args: EntryHello, prefix: &ResPrefix) -> Next { + let name = args.inner.first().cloned().unwrap_or_default(); + ResultGreeting::new(format!("{}, {}", prefix.0, name)).to_render() +} + +#[test] +fn test_handle_with_resource() { + // Resources need to be passed manually in tests + let result = handle_hello( + EntryHello::new(vec!["World".to_string()]), + &ResPrefix("Hello".to_string()), + ); + let greeting = unpack_chain_process!(result, ResultGreeting, ThisProgram); + assert_eq!(greeting.inner, "Hello, World"); +} +``` + +The pipeline model makes testing simple: each Chain and Renderer is a relatively independent function — construct input, assert output. + +<p align="center" style="font-size: 0.85em; color: clear;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/2-define-a-dispatcher.md b/docs/pages/2-define-a-dispatcher.md new file mode 100644 index 0000000..804ad1b --- /dev/null +++ b/docs/pages/2-define-a-dispatcher.md @@ -0,0 +1,103 @@ +<h1 align="center">Declare a Dispatcher</h1> +<p align="center"> + Use the <code>dispatcher!</code> macro to declare commands and register them +</p> + +Mingling's pipeline starts with a Dispatcher. + +Its job is simple: **match the user's input command, wrap the arguments into an Entry type**. + +## The `dispatcher!` Macro + +The `dispatcher!` macro generates two types at once: + +| Generated type | Purpose | +| -------------- | -------------------------------------------------------------- | +| `CMDType` | The dispatcher itself, needs to be registered to Program | +| `EntryType` | The entry type, wraps `Vec<String>`, serves as input for Chain | + +The syntax is a fixed three-part pattern: + +```rust +dispatcher!("command path", DispatcherType => EntryType); +``` + +Here's a concrete example: + +```rust +dispatcher!("greet", CMDGreet => EntryGreet); +``` + +> [!NOTE] +> The command name (`"greet"`) is auto-converted to kebab-case. Even if you write `"GreetUser"`, matching will use `greet-user`. + +## Registering with Program + +Once you have a dispatcher, you need to tell Program about it: + +```rust +@@@ dispatcher!("greet", CMDGreet => EntryGreet); +@@@ fn main() { +@@@ let mut program = ThisProgram::new(); +// Register the dispatcher +program.with_dispatcher(CMDGreet); +@@@ } +@@@ gen_program!(); +``` + +> [!TIP] +> If you have many commands, use `with_dispatchers` to register multiple at once: `program.with_dispatchers((CMDGreet, CMDAdd, CMDRemoteRm))`. + +## Multi-level Commands + +If your program has a hierarchy — e.g., `remote add`, `remote rm` — just separate the command name with dots: + +```rust +dispatcher!("remote.add", CMDRemoteAdd => EntryRemoteAdd); +dispatcher!("remote.rm", CMDRemoteRm => EntryRemoteRm); +``` + +When the user types `remote add` in the terminal, Mingling matches `remote` and `add` as two levels in sequence. + +## The Entry Type `EntryGreet` + +You might be curious about what's inside `EntryGreet`. It's essentially a struct wrapping `Vec<String>`: + +```rust +// Illustration of code generated by the dispatcher! macro +pub struct EntryGreet { + pub inner: Vec<String>, +} +``` + +When the user types `greet Alice Bob` on the command line, `EntryGreet.inner` becomes `vec!["Alice", "Bob"]`. + +> [!IMPORTANT] +> Entry's `inner` only contains **the remaining args after matching**. +> +> Take `remote add origin` as an example: `remote` and `add` are used for matching the command path, only `origin` goes into `EntryRemoteAdd.inner`. + +## Advanced: Implicit Declaration + +The above is the standard syntax. If you enable the `extra_macros` feature, you can be more concise: + +```rust +// Features: ["extra_macros"] +// Omit CMDType and EntryType, names are auto-derived + dispatcher!("greet"); +// dispatcher!("greet", CMDGreet => EntryGreet); +``` + +This syntax auto-generates `CMDGreet` and `EntryGreet`, with the same effect as the explicit declaration. + +But for the tutorial, we'll stick with explicit syntax — it's clearer and doesn't require extra features. + +See [Feature List](pages/other/features) for details. + +## Next Step + +Next we'll write a Chain to receive the Entry and handle the actual business logic. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/3-define-a-chain.md b/docs/pages/3-define-a-chain.md new file mode 100644 index 0000000..c6d3f8e --- /dev/null +++ b/docs/pages/3-define-a-chain.md @@ -0,0 +1,131 @@ +<h1 align="center">Declare a Chain</h1> +<p align="center"> + Use the <code>chain</code> macro to declare a chain and handle Entry input +</p> + +In the previous section, we declared `dispatcher!("greet", CMDGreet => EntryGreet)`. + +Now when a user types `greet`, it gets matched and wrapped into `EntryGreet`. + +But what happens after we get the Entry? + +We need a Chain to process it. + +## The `#[chain]` Macro + +`#[chain]` marks a handler function. The format is straightforward: + +```rust +@@@dispatcher!("greet", CMDGreet => EntryGreet); +pack!(ResultName = String); + +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + // args contains the remaining params after matching user input + let name = args.inner.first().cloned().unwrap_or_else(|| "World".to_string()); + // Wrap the result into Next, telling the dispatcher where to go next + ResultName::new(name) +} +``` + +Notice anything? + +The Chain function signature declares what it needs — `args: EntryGreet`. + +Then it returns a newtype via `ResultName::new(name)`. + +This returned `Next` expands into `impl Into<ChainProcess<ThisProgram>>`. + +> [!TIP] +> Wondering how `Into<ChainProcess<G>>` works? +> +> Check out the [Any Output Mechanism](pages/concepts/3-any-output) chapter to learn about `ChainProcess`. + +## The `pack!` Macro + +You've probably guessed it — `pack!(ResultName = String)` defines a type that flows through the pipeline: + +```rust +// pack!(ResultName = String) generates code roughly like this + +#[derive(Groupped)] +pub struct ResultName { + pub inner: String, +} +``` + +Think of it as a **tagged** `String`. + +The dispatcher uses this tag for precise routing, ensuring data doesn't get mixed up — e.g., data sent to `RenderGreet` won't be misdelivered to `RenderError`. + +> [!NOTE] +> Unlike a simple type alias (`type`), `pack!` generates a completely new type with its own `TypeId`. + +Here's a recommended naming convention: + +| Role | Naming Pattern | Example | +| ------------ | ---------------------- | -------------------- | +| Entry | `Entry` + command | `EntryGreet` | +| Intermediate | `State` + description | `StateParsedArgs` | +| Result | `Result` + description | `ResultGreetSomeone` | +| Error | `Error` + description | `ErrorUserNotFound` | + +See [Naming Convention](pages/other/naming_rule) for details, but for now just remember: **use `pack!` to give your data a meaningful name**. + +## Extracting Params from Entry + +`EntryGreet`'s `inner` is a `Vec<String>`, which you can freely process inside a Chain: + +```rust +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + // Take the first param, or use a default + let name = args + .inner + .first() + .cloned() + .unwrap_or_else(|| "World".to_string()); + + ResultName::new(name) +} +``` + +If you enable the `parser` feature, you can also use `Picker` for more flexible param extraction — but that's a topic for later. + +## Putting It Together + +Now let's connect the Dispatcher and Chain: + +```rust +// 1. Declare the command +dispatcher!("greet", CMDGreet => EntryGreet); + +// 2. Declare the pipeline data type +pack!(ResultName = String); + +// 3. Processing logic +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + let name = args.inner + .first() + .cloned() + .unwrap_or_else(|| "World".to_string()); + ResultName::new(name) +} + +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} + +gen_program!(); +``` + +But this code isn't complete yet — we only have the Dispatcher and Chain. One last step remains: **rendering the result**. That's what the next chapter, Renderer, covers. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/4-render-result.md b/docs/pages/4-render-result.md new file mode 100644 index 0000000..b1365b8 --- /dev/null +++ b/docs/pages/4-render-result.md @@ -0,0 +1,151 @@ +<h1 align="center">Rendering Results</h1> +<p align="center"> + Declare a renderer using the <code>#[renderer]</code> macro to output results. +</p> + +Now we've created a Dispatcher and a Chain, and produced a Result type via `pack!`. The final step: **present the result to the user**. + +## The `#[renderer]` Macro + +Similar to `#[chain]`, `#[renderer]` marks an output function: + +```rust +use std::io::Write; + +pack!(ResultName = String); +#[renderer] +fn render_name(name: ResultName) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Hello, {}!", *name).ok(); + result +} +``` + +A Renderer takes the result produced by a Chain and returns a `RenderResult`. Inside the function, create a `RenderResult`, write content using `write!` / `writeln!` (from [`std::io::Write`](https://doc.rust-lang.org/std/io/trait.Write.html)), and return it. + +## The `RenderResult` Type + +`RenderResult` is a buffer type that holds rendered text and an exit code. Instead of writing directly to the terminal, it writes content into an internal buffer. This approach gives us: + +1. **Exit code support**—you can set the program to exit with a specific exit code +2. **Testability**—rendered output can be captured and asserted against +3. **Post-processing**—the result can be captured and further processed uniformly + +## A Complete Runnable Program + +Putting all three tutorials together, here's your first complete Mingling program: + +```rust +use std::io::Write; + +// 1. Declare commands with a Dispatcher +dispatcher!("greet", CMDGreet => EntryGreet); + +// 2. Declare result data with pack! +pack!(ResultName = String); + +// 3. Handle logic with a Chain +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + let name = args.inner + .first() + .cloned() + .unwrap_or_else(|| "World".to_string()); + ResultName::new(name) +} + +// 4. Output results with a Renderer +#[renderer] +fn render_name(name: ResultName) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Hello, {}!", *name).ok(); + result +} + +// 5. Assemble and run the program in main +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} + +// 6. Use gen_program! to generate the full program +gen_program!(); +``` + +## Try It Out + +```bash +~# cargo run -- greet Alice +``` + +```text +Hello, Alice! +``` + +Try without arguments: + +```bash +~# cargo run -- greet +``` + +```text +Hello, World! +``` + +Try a non-existent command: + +```bash +cargo run -- great +``` + +```text +# No output! +``` + +## Adding a Fallback + +`gen_program!()` auto-generates an `ErrorDispatcherNotFound` type wrapping `Vec<String>`—it holds the user input that didn't match any command. You just need to write a Renderer for it: + +```rust +use std::io::Write; + +#[renderer] +fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult { + let mut result = RenderResult::new(); + if err.inner.is_empty() { + writeln!(result, "Unknown command").ok(); + } else { + writeln!(result, "Command not found: \"{}\"", err.inner.join(" ")).ok(); + } + result +} +``` + +With that added, try the non-existent command again: + +```bash +cargo run -- great +``` + +```text +Command not found: "great" +``` + +## Congratulations + +You've completed your first full Mingling program! Let's recap what you've learned: + +| Concept | Macro / Function | One-liner | +| -------------- | ---------------- | --------------------------------------- | +| Declare cmds | `dispatcher!` | Tell the program what the user can type | +| Handle logic | `#[chain]` | What to do when args are received | +| Output results | `#[renderer]` | How to present results to the user | +| Type wrapping | `pack!` | Give your data a meaningful name | +| Program entry | `gen_program!()` | Auto-generate the pipeline wiring | + +In real projects you'll also use advanced features like resource injection, hooks, completions, REPL, etc., but the core skeleton stays the same: **Dispatcher → Chain → Renderer**. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/5-multiple-commands.md b/docs/pages/5-multiple-commands.md new file mode 100644 index 0000000..a56f7b0 --- /dev/null +++ b/docs/pages/5-multiple-commands.md @@ -0,0 +1,113 @@ +<h1 align="center">Multi-Command Program</h1> +<p align="center"> + Adding multiple commands to a single program +</p> + +Real-world CLIs rarely have just one command. Let's extend our previous greet program by adding a second command, and see what a multi-command program looks like. + +## Adding a Second Command + +Work in the same project: + +```rust +// Declare two commands +dispatcher!("greet", CMDGreet => EntryGreet); +dispatcher!("add", CMDAdd => EntryAdd); + +pack!(ResultGreeting = String); +pack!(ResultSum = i32); + +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + let name = args.inner.first().cloned().unwrap_or_else(|| "World".to_string()); + ResultGreeting::new(name) +} + +#[chain] +fn handle_add(args: EntryAdd) -> Next { + let sum: i32 = args.inner.iter().filter_map(|s| s.parse::<i32>().ok()).sum(); + ResultSum::new(sum) +} + +#[renderer] +fn render_greet(result: ResultGreeting) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Hello, {}!", *result).ok(); + r +} + +#[renderer] +fn render_sum(result: ResultSum) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Sum: {}", *result).ok(); + r +} + +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatchers((CMDGreet, CMDAdd)); + program.exec_and_exit(); +} + +gen_program!(); +``` + +Both commands share the same pipeline model, but each has its own path: + +```text +> my-cli greet Alice +Hello, Alice! +> my-cli add 1 2 3 +Sum: 6 +``` + +## Registering Multiple Dispatchers + +Notice `with_dispatchers`? When you need to register multiple dispatchers, just pass them as a tuple: + +```rust +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@dispatcher!("add", CMDAdd => EntryAdd); +@@@pack!(ResultGreeting = String); +@@@pack!(ResultSum = i32); +@@@#[chain] fn handle_greet(_args: EntryGreet) -> Next { ResultGreeting::new("ok".into()) } +@@@#[renderer] fn render_greet(_greeting: ResultGreeting) -> RenderResult { RenderResult::new() } +@@@#[chain] fn handle_add(_args: EntryAdd) -> Next { ResultSum::new(0) } +@@@#[renderer] fn render_sum(_sum: ResultSum) -> RenderResult { RenderResult::new() } +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatchers((CMDGreet, CMDAdd)); + program.exec_and_exit(); +} +``` + +This is equivalent to registering them one by one, same effect. + +> [!TIP] +> The tuple supports up to 7 dispatchers. For more than 7, chain `with_dispatcher` calls instead. + +## Subcommands + +Multi-level commands work the same way—each dot-separated level is just part of the name: + +```rust +dispatcher!("remote.add", CMDRemoteAdd => EntryRemoteAdd); +dispatcher!("remote.rm", CMDRemoteRm => EntryRemoteRm); +``` + +Each subcommand's Entry, Chain, and Renderer are completely independent and don't interfere. + +## Type Independence + +Notice we used two different `pack!` macros: + +- `pack!(ResultGreeting = String)` +- `pack!(ResultSum = i32)` + +They are independent types, and `gen_program!()` assigns them different enum variants. + +The dispatcher will never route `ResultGreeting` data to `render_sum` — **type safety is guaranteed from the naming stage**. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/6-argument-parse-picker.md b/docs/pages/6-argument-parse-picker.md new file mode 100644 index 0000000..c80c25c --- /dev/null +++ b/docs/pages/6-argument-parse-picker.md @@ -0,0 +1,405 @@ +<h1 align="center">Parsing Arguments with Picker</h1> +<p align="center"> + Use Picker to perform basic argument parsing +</p> + +In previous tutorials, we extracted args manually from `EntryGreet.inner` (`Vec<String>`). + +```rust +@@@ fn main() { +@@@ let args : Vec<String> = vec![]; +let name = args.first().cloned().unwrap_or_else(|| "World".to_string()); +@@@ } +``` + +But this approach doesn't scale well for many params. Mingling provides `Picker` — a chaining API to extract and convert args. + +To enable `Picker`, update your `Cargo.toml`: + +```toml +# Cargo.toml +[dependencies.mingling] +features = ["parser"] +``` + +Now let's look at `Picker` in action: + +```rust +// Features: ["parser"] +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); + +#[chain] +fn handle_greet_entry(prev: EntryGreet) -> Next { + let name = prev.pick_or((), "World").unpack(); + ResultName::new(name) +} +``` + +`AsPicker` implements `pick`, `pick_or`, and `pick_or_route` for any type that can convert to `Vec<String>`: they semantically **pick** args from a string list and convert them to structured data. + +Breaking down the example above: + +```rust +// Features: ["parser"] +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +@@@#[chain] +@@@fn handle_greet_entry(prev: EntryGreet) -> Next { +let name = prev.pick_or((), "World").unpack(); +@@@ResultName::new(name) +@@@} +``` + +Its semantics are: + +```rust +// Features: ["parser"] +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +@@@#[chain] +@@@fn handle_greet_entry(prev: EntryGreet) { +@@@let name: String = + prev.pick_or((), "World").unpack(); +// ~~~~ ~~~~~~~ ~~ ~~~~~~~ ~~~~~~~~ +// | | | | |_ unpack to String +// | | | |__________ default value "World" +// | | |______________ pick the first positional arg (no flag) +// | |______________________ pick or use default +// |___________________________ from previous input +@@@} +``` + +## Parsing Flag Args + +If your program needs to parse flag args (e.g., `greet --name Alice`), do this: + +```rust +// Features: ["parser"] +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); + +#[chain] +fn handle_greet_entry(prev: EntryGreet) -> Next { + let name = prev.pick_or(["--name", "-n"], "World").unpack(); + ResultName::new(name) +} +``` + +Its semantics: + +```rust +// Features: ["parser"] +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +@@@#[chain] +@@@fn handle_greet_entry(prev: EntryGreet) { +@@@let name: String = + prev.pick_or(["--name", "-n"], "World").unpack(); +// ~~~~ ~~~~~~~ ~~~~~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~ +// | | | | |_ unpack to String +// | | | |__________ default value "World" +// | | |____________________________ pick arg after "--name" or "-n" +// | |____________________________________ pick or use default +// |_________________________________________ from previous input +@@@} +``` + +## About `.unpack()` + +You may have noticed that `Picker` calls `.unpack()` at the end of parsing. It converts the accumulated parse results into structured info. + +For a single pick, `.unpack()` returns a single value; for multiple picks, it returns a tuple: + +```rust +// Features: ["parser"] +@@@dispatcher!("test", CMDTest => EntryTest); +@@@pack!(ResultInfo = (String, u8, u32)); + +#[chain] +fn handle_test_entry(prev: EntryTest) -> Next { + let (name, age, id) = prev + .pick::<String>(["--name", "-n"]) + .pick::<u8>(["--age", "-a"]) + .pick::<u32>(["--id", "-I"]) + .unpack(); + + ResultInfo::new((name, age, id)) +} +``` + +> [!IMPORTANT] +> `Picker` is very sensitive to parse order, esp. for positional args (they're parsed sequentially). If you need to parse positional args, make sure all **flag args** have been picked and consumed first. + +## Handling Edge Cases with `pick_or_route` + +As the old saying goes: "Never trust your users." To handle missing required args, type mismatches, etc., `pick_or_route` routes the execution chain to a dedicated error-handling type. + +A simple example: + +```rust +// Features: ["parser", "extra_macros"] +@@@use mingling::macros::route; +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +@@@pack!(ErrorNoName = ()); + +#[chain] +fn handle_greet_entry(prev: EntryGreet) -> Next { + let pick_result = prev + .pick_or_route(["--name", "-n"], ErrorNoName::default()) + .unpack(); + + // Use route! macro to unpack pick_result + let name = route!(pick_result); + ResultName::new(name).into() +} + +#[renderer] +fn render_no_name(_prev: ErrorNoName) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Error: No name provided.").ok(); + r +} + +#[renderer] +fn render_name(prev: ResultName) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Hello, {}!", *prev).ok(); + r +} +``` + +With `pick_or_route`, the code gets a bit more complex: `.unpack()` no longer returns the value directly, but `Result<Value, Route>`. + +However, Mingling's `extra_macros` feature provides the `route!` macro to simplify unwrapping — it just omits some boilerplate: + +```rust +// Features: ["parser", "extra_macros"] +@@@ pack!(ErrorFail = ()); +@@@ use mingling::macros::route; +@@@ fn func() -> mingling::ChainProcess<ThisProgram> { +@@@ let args: Vec<String> = vec![]; +@@@ let pick_result = args.pick_or_route::<String, _>((), ErrorFail::new(())).unpack(); +let name = route!(pick_result); +@@@ mingling::macros::empty_result!() +@@@ } +``` + +It expands to: + +```rust +// Features: ["parser", "extra_macros"] +@@@ pack!(ErrorFail = ()); +@@@ fn func() -> mingling::ChainProcess<ThisProgram> { +@@@ let args: Vec<String> = vec![]; +@@@ let pick_result = args.pick_or_route::<String, _>((), ErrorFail::new(())).unpack(); +let name = match pick_result { + Ok(r) => r, + Err(e) => return e.to_chain(), +}; +@@@ mingling::macros::empty_result!() +@@@ } +``` + +## Post-processing Extracted Values + +After picking user input, you can use `after` to process the value immediately: + +```rust +// Features: ["parser"] +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); + +#[chain] +fn handle_greet_entry(prev: EntryGreet) -> Next { + let name = prev + .pick_or(["--name", "-n"], "World") + // Format immediately after extracting --name + .after(|name: String| { + name.replace(['-', '_', '.'], " ") + .to_lowercase() + .trim() + .to_string() + }) + .unpack(); + + ResultName::new(name) +} +``` + +Similarly, use `after_or_route` to handle format errors in input args: + +```rust +// Features: ["parser", "extra_macros"] +@@@use mingling::macros::route; +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultName = String); +@@@pack!(ErrorNameTooLong = usize); + +#[chain] +fn handle_greet_entry(prev: EntryGreet) -> Next { + let pick_result = prev + .pick_or(["--name", "-n"], "World") + .after_or_route(|name: &String| { + if name.len() < 32 { + Ok(name.clone()) + } else { + Err(ErrorNameTooLong::new(name.len())) + } + }) + .unpack(); + let name = route!(pick_result); + + ResultName::new(name).into() +} + +#[renderer] +fn render_name_too_long(prev: ErrorNameTooLong) -> RenderResult { + let mut r = RenderResult::new(); + let len = *prev; + writeln!(r, "Error: name too long (length: {} > 32)", len).ok(); + r +} + +#[renderer] +fn render_name(prev: ResultName) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Hello, {}!", *prev).ok(); + r +} +``` + +## Boolean Parsing + +`Picker` can parse bools too, with two modes: implicit and explicit. + +| Mode | Format | +| -------- | ----------------------------------- | +| Implicit | `--confirmed` | +| Explicit | `--confirm true` or `--confirm yes` | + +- Using `.pick::<bool>(flag)` → implicit: flag present means `true` +- Using `.pick::<Yes>(flag)` or `.pick::<True>(flag)` → explicit + +Implicit is fine for most cases, but for important confirmations, explicit logic is more semantic. + +```rust +// Features: ["parser"] +@@@use mingling::parser::Yes; +@@@dispatcher!("test", CMDTest => EntryTest); +@@@pack!(ResultDone = ()); + +#[chain] +fn handle_entry(prev: EntryTest) -> Next { +@@@ let prev1 = prev.clone(); + let _confirmed: bool = prev.pick::<Yes>(()).unpack().is_yes(); +@@@ let prev = prev1; + let _confirm: bool = prev.pick::<bool>(["--confirm", "-C"]).unpack(); + ResultDone::default().to_render() +} +``` + +## Special Usage: `usize` Parsing + +Mingling provides a special `usize` feature: parsing strings like `25G`, `32mib`, etc. + +```rust +// Features: ["parser"] + +#[test] +fn parse_size() { + let vec = vec!["--size".to_string(), "25mib".to_string()]; + let size: usize = vec.pick(["--size", "-S"]).unpack(); + assert_eq!(size, 25 * 1024 * 1024); +} +``` + +## Custom Parseable Types + +Implement the `Pickable` trait to make your type parseable by `Picker` — this is where Picker's extensibility comes from. + +```rust +// Features: ["parser"] +@@@use mingling::parser::{Pickable, Argument}; +@@@use mingling::Flag; +#[derive(Default, Clone)] +pub struct Address { + ip: String, + port: u16, +} + +impl Pickable for Address { + type Output = Self; + fn pick(args: &mut Argument, flag: Flag) -> Option<Self::Output> { + let raw = args.pick_argument(flag)?; + 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 }) + } +} +@@@dispatcher!("connect", CMDConnect => EntryConnect); +@@@pack!(ResultConnected = Address); + +#[chain] +fn handle_connect_entry(prev: EntryConnect) -> Next { + let address: Address = prev.pick("--addr").unpack(); + ResultConnected::new(address) +} + +#[renderer] +fn render_connected(prev: ResultConnected) -> RenderResult { + let mut r = RenderResult::new(); + let addr = prev.inner; + writeln!(r, "Connected: IP: {} PORT: {}", addr.ip, addr.port).ok(); + r +} +``` + +Output: + +```text +~# my-cli connect --addr 127.0.0.1:8080 +Connected: IP: 127.0.0.1 PORT: 8080 +``` + +## Auto-implementing Pickable for Enums + +To implement `Pickable` for an enum, just make it implement `EnumTag`, then implement `PickableEnum`: + +```rust +// Features: ["parser"] +@@@use mingling::parser::PickableEnum; +@@@use mingling::EnumTag; +#[derive(Debug, Default, EnumTag)] +pub enum Fruits { + #[default] + Apple, + Banana, + Orange, +} + +impl PickableEnum for Fruits {} +@@@dispatcher!("eat", CMDEat => EntryEat); +@@@pack!(ResultFruit = Fruits); + +#[chain] +fn handle_eat_entry(prev: EntryEat) -> Next { + let fruit: Fruits = prev.pick("--fruit").unpack(); + ResultFruit::new(fruit) +} + +#[renderer] +fn render_fruit(prev: ResultFruit) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Picked fruit: {:?}", *prev).ok(); + r +} +``` + +That covers all the features of `Picker`. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/7-argument-parse-clap.md b/docs/pages/7-argument-parse-clap.md new file mode 100644 index 0000000..b11e00e --- /dev/null +++ b/docs/pages/7-argument-parse-clap.md @@ -0,0 +1,92 @@ +<h1 align="center">Parsing Arguments with Clap</h1> +<p align="center"> + Use clap for more complex argument parsing +</p> + +Picker is suitable for lightweight arg extraction, but when there are many args, complex validation rules, or you need auto-generated `--help`, you can use [clap](https://crates.io/crates/clap). + +## Enable clap feature + +```toml +[dependencies.mingling] +features = ["clap"] + +[dependencies.clap] +version = "4" +features = ["derive", "color"] +``` + +## dispatcher_clap + +Add `#[dispatcher_clap]` on a `clap::Parser` struct to auto-generate a Dispatcher: + +```rust +// Features: ["clap"] +// Dependencies: +// clap = "4" +@@@ use mingling::macros::dispatcher_clap; +#[derive(Default, clap::Parser, Groupped)] +#[dispatcher_clap("greet", CMDGreet, help = true, error = ErrorGreetParsed)] +pub struct EntryGreet { + #[clap(default_value = "World")] + name: String, + #[arg(short, long, default_value_t = 1)] + repeat: i32, +} + +#[renderer] +fn render_greet(greet: EntryGreet) -> RenderResult { + let mut r = RenderResult::new(); + let count = greet.repeat.max(0) as usize; + write!(r, "Hello, ").ok(); + for _ in 0..count { + write!(r, "{} ", greet.name).ok(); + } + writeln!(r, "!").ok(); + r +} + +#[renderer] +fn render_greet_parse_failed(err: ErrorGreetParsed) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "{}", *err).ok(); + r +} +``` + +## Working with BasicProgramSetup + +If you need `--help` support, register `BasicProgramSetup` in main and set the clap help output mode: + +```rust +// Features: ["clap"] +// Dependencies: +// clap = "4" +@@@use mingling::setup::BasicProgramSetup; +@@@use mingling::macros::dispatcher_clap; +@@@#[derive(Default, clap::Parser, Groupped)] +@@@#[dispatcher_clap("greet", CMDGreet)] +@@@pub struct EntryGreet { +@@@ name: String, +@@@} +@@@#[renderer] +@@@fn render_greet(greet: EntryGreet) -> RenderResult { +@@@ let mut r = RenderResult::new(); +@@@ write!(r, "Hello, {}!", greet.name).ok(); +@@@ r +@@@} +fn main() { + let mut program = ThisProgram::new(); + program.with_setup(BasicProgramSetup); + program.stdout_setting.clap_help_print_behaviour = + mingling::ClapHelpPrintBehaviour::WriteToRenderResult; + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} +``` + +See [example-clap-binding](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-clap-binding) for more details. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/8-setup-and-resources.md b/docs/pages/8-setup-and-resources.md new file mode 100644 index 0000000..3858e99 --- /dev/null +++ b/docs/pages/8-setup-and-resources.md @@ -0,0 +1,89 @@ +<h1 align="center">Program Setup</h1> +<p align="center"> + Initialize your program with Setup +</p> + +When a program needs to do some init work at startup—like parsing global args or registering resources—you can organize that logic with `#[program_setup]`. + +## Initialize with Setup + +```rust +// Features: ["extra_macros"] +@@@use mingling::macros::program_setup; +@@@use mingling::Program; +#[program_setup] +fn my_setup(program: &mut Program<ThisProgram>) { + // Extract global flag from args + program.global_flag(["-v", "--verbose"], |program| { + program.stdout_setting.verbose = true; + }); +} +@@@ +@@@fn main() { +@@@ let mut program = ThisProgram::new(); +@@@ program.with_setup(MySetup); +@@@ program.exec_and_exit(); +@@@} +@@@gen_program!(); +``` + +A function annotated with `#[program_setup]` receives `&mut Program<ThisProgram>`, where you can do any init work. + +Register it in `main` via `program.with_setup(...)` to use it. + +> [!NOTE] +> `#[program_setup]` requires the `extra_macros` feature. Without it, you can manually implement the `ProgramSetup` trait. + +## Extract Global Args + +The most common use of Setup is extracting global args. Mingling provides a few helper methods: + +```rust +// Features: ["extra_macros"] +@@@use mingling::macros::program_setup; +@@@use mingling::Program; +#[program_setup] +fn my_setup(program: &mut Program<ThisProgram>) { + // Boolean flag + program.global_flag(["-v", "--verbose"], |program| { + program.stdout_setting.verbose = true; + }); + + // Flag with a value + program.global_argument("--name", |_program, value| { + // value is "Alice" + let _ = value; + }); +} +``` + +> [!TIP] +> `global_flag` and `global_argument` automatically remove matched args from `program.args`, so they won't enter the pipeline. + +## Built-in Setup + +Mingling provides several ready-to-use Setups covering the most common CLI program needs: + +| Setup | Functionality | +| --------------------------- | ------------------------------------------------------------------------------------------ | +| `BasicProgramSetup` | Parses `--help`/`-h`, `--quiet`/`-q`, `--confirm`/`-C` | +| `DirectoryEnvironmentSetup` | Registers directory resources: current dir, executable dir, home dir, temp dir | +| `ExitCodeSetup` | Controls program exit code via `ResExitCode` | +| `StructuralRendererSetup` | Enables `--json`, `--yaml` etc. structured output (requires `structural_renderer` feature) | + +Usage is just one line in `main`: + +```rust +@@@use mingling::setup::BasicProgramSetup; +fn main() { + let mut program = ThisProgram::new(); + program.with_setup(BasicProgramSetup); + program.exec_and_exit(); +} +``` + +`BasicProgramSetup` handles common params that most CLI programs need, saving you the trouble of manual parsing. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/9-error-handling.md b/docs/pages/9-error-handling.md new file mode 100644 index 0000000..3e004f2 --- /dev/null +++ b/docs/pages/9-error-handling.md @@ -0,0 +1,128 @@ +<h1 align="center">Error Handling</h1> +<p align="center"> + Gracefully present errors to the user +</p> + +A pipeline isn't just the happy path. When input is invalid, a resource isn't found, or an operation fails, you need a place to handle these "surprises" instead of letting the program panic. + +## Two Paths: Success vs. Error + +Recall the pipeline model: Chain's return value is `Next`, which has two destinations: + +| Route | Meaning | +| -------------- | ------------------------------------------- | +| `.to_render()` | Got a result, hand it to a Renderer to show | +| `.to_chain()` | Not done yet, hand it to the next Chain | + +Error values can also take either path—you can render the error msg directly, or pass it to the next Chain for potential recovery. + +## Distinguish Errors with Dedicated Types + +```rust +@@@dispatcher!("greet", CMDGreet => EntryGreet); +pack!(ResultGreeting = String); +pack!(ErrorNameEmpty = String); + +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + let name = args.inner.first().cloned().unwrap_or_default(); + + if name.is_empty() { + ErrorNameEmpty::new("name is required".to_string()).to_render() + } else { + ResultGreeting::new(name).to_render() + } +} +``` + +Then write separate Renderers: + +```rust +@@@dispatcher!("greet", CMDGreet => EntryGreet); +@@@pack!(ResultGreeting = String); +@@@pack!(ErrorNameEmpty = String); +@@@#[chain] fn handle_greet(args: EntryGreet) -> Next { ResultGreeting::new(args.inner.first().cloned().unwrap_or_default()).to_render() } + +#[renderer] +fn render_greeting(result: ResultGreeting) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Hello, {}!", *result).ok(); + r +} + +#[renderer] +fn render_error_name_empty(err: ErrorNameEmpty) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Error: {}", *err).ok(); + r +} +``` + +Each Renderer does its own job; what the user sees depends on what the Chain returned. + +## Complete Example + +```rust +dispatcher!("greet", CMDGreet => EntryGreet); + +pack!(ResultGreeting = String); +pack!(ErrorNameEmpty = String); + +#[chain] +fn handle_greet(args: EntryGreet) -> Next { + let name = args.inner.first().cloned().unwrap_or_default(); + if name.is_empty() { + ErrorNameEmpty::new("name is required".to_string()).to_render() + } else { + ResultGreeting::new(name).to_render() + } +} + +#[renderer] +fn render_greeting(result: ResultGreeting) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Hello, {}!", *result).ok(); + r +} + +#[renderer] +fn render_error_name_empty(err: ErrorNameEmpty) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Error: {}", *err).ok(); + r +} + +fn main() { + let mut program = ThisProgram::new(); + program.with_dispatcher(CMDGreet); + program.exec_and_exit(); +} + +gen_program!(); +``` + +Output: + +```text +~# my-cli greet Alice +Hello, Alice! + +~# my-cli greet +Error: name is required +``` + +## About `pack_err!` + +If you've enabled `extra_macros`, you can use `pack_err!` to quickly declare an error type with an auto-generated `name` field: + +```rust +// Features: ["extra_macros"] +pack_err!(ErrorNotFound); +// Generates: struct ErrorNotFound { pub name: String } +``` + +See [Feature List](pages/other/features) for details. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/advanced/.name b/docs/pages/advanced/.name new file mode 100644 index 0000000..a8a2c7e --- /dev/null +++ b/docs/pages/advanced/.name @@ -0,0 +1 @@ +Advanced diff --git a/docs/pages/advanced/1-completion.md b/docs/pages/advanced/1-completion.md new file mode 100644 index 0000000..a90c3ce --- /dev/null +++ b/docs/pages/advanced/1-completion.md @@ -0,0 +1,83 @@ +<h1 align="center">Completion</h1> +<p align="center"> + Fully dynamic completion system via the `comp` feature +</p> + +Mingling's completion is **fully dynamic** — no static completion files, suggestions are computed at runtime based on the user's current input. + +## Enable `comp` + +```toml +# Cargo.toml +[dependencies.mingling] +features = ["comp"] + +[build-dependencies.mingling] +features = [ + "comp", + # Enable `builds` for build-time support + "builds" +] +``` + +## How it works + +When the user presses `TAB`, the completion script calls the program's hidden subcommand `__comp`, which dynamically queries the best suggestions based on the provided `ShellContext`. + +This hidden subcommand is auto-generated by `gen_program!()` when the `comp` feature is enabled. Its dispatcher is `CMDCompletion` — you need to add it to your program via `with_dispatcher`. + +Completion flow: + +1. Re-match the user's current input to a `Dispatcher` +2. Call the corresponding `#[completion]` function +3. The function returns a `Suggest` (file completion or a list of suggestions) +4. Notify the shell to display the suggestions + +## Define completions + +Use `#[completion(EntryType)]` to define completion logic for an Entry: + +```rust +// Features: ["comp"] +@@@use mingling::prelude::*; +@@@use mingling::{ShellContext, Suggest, SuggestItem}; +@@@use std::collections::BTreeSet; +@@@dispatcher!("greet", CMDGreet => EntryGreet); + +#[completion(EntryGreet)] +fn complete_greet(ctx: &ShellContext) -> Suggest { + if ctx.previous_word == "greet" { + let mut items = BTreeSet::new(); + items.insert(SuggestItem::new_with_desc("Alice".into(), "Likes to receive messages".into())); + items.insert(SuggestItem::new("World".into())); + Suggest::Suggest(items) + } else { + Suggest::FileCompletion + } +} +``` + +The `suggest!` macro is a more concise way to write the same thing: + +```rust +// Features: ["comp"] +@@@use mingling::macros::suggest; +@@@fn example() { +suggest! { + "Alice": "Likes to receive messages", + "World" +}; +@@@} +``` + +`ShellContext` holds the user's current input state (`previous_word`, `current_word`, `all_words`, etc.). `Suggest` has two variants: `Suggest::Suggest(list)` returns a suggestion list, `Suggest::FileCompletion` delegates file completion to the shell. + +## Generate completion scripts + +Call `build_comp_scripts` in `build.rs` to generate completion scripts (requires `builds` + `comp` features). + +See [example-completion](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-completion). + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/advanced/2-structural-renderer.md b/docs/pages/advanced/2-structural-renderer.md new file mode 100644 index 0000000..f31f758 --- /dev/null +++ b/docs/pages/advanced/2-structural-renderer.md @@ -0,0 +1,124 @@ +<h1 align="center">Structural Rendering</h1> +<p align="center"> + Use the <code>structural_renderer</code> feature to render output as serialized text +</p> + +With `structural_renderer` enabled, your program can switch output to a structured format via `--json`, `--yaml`, etc., making it easy to integrate with other tools. + +## Enabling the Feature + +```toml +[dependencies.mingling] +features = ["structural_renderer"] +``` + +`structural_renderer` automatically enables `json_serde_fmt`. + +For more formats, enable `structural_renderer_full` (includes JSON, YAML, TOML, RON). + +> [!NOTE] +> To customize output types, see [Features](./pages/other/features) + +## Basic Usage + +After enabling `StructuralRendererSetup`, use `pack_structural!` instead of `pack!` to declare types that support structured output: + +```rust +// Features: ["structural_renderer"] +// Dependencies: +// serde = "1" +@@@use mingling::setup::StructuralRendererSetup; +@@@dispatcher!("render", CMDRender => EntryRender); + +// pack_structural! is equivalent to pack! + StructuralData +pack_structural!(ResultInfo = (String, i32)); + +#[chain] +fn handle_render(args: EntryRender) -> Next { + let name = args.inner.first().cloned().unwrap_or_default(); + let age = args.inner.get(1).and_then(|s| s.parse().ok()).unwrap_or(0); + ResultInfo::new((name, age)) +} + +#[renderer] +fn render_info(r: ResultInfo) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "{:?}", *r).ok(); + result +} +``` + +Output: + +```text +~# my-cli render Bob 22 +("Bob", 22) + +~# my-cli render Bob 22 --json +{"inner":["Bob",22]} +``` + +When the user passes `--json`, the framework automatically serializes the render result as JSON — no business logic changes needed. + +## Customizing Output Structure + +The default output from `pack_structural!` includes an `inner` field. For full control over the output structure, define the type manually with `#[derive(StructuralData, Serialize, Groupped)]`: + +```rust +// Features: ["structural_renderer"] +// Dependencies: +// serde = "1" +@@@use mingling::prelude::*; +@@@use mingling::setup::StructuralRendererSetup; +@@@use mingling::StructuralData; +@@@use serde::Serialize; +@@@dispatcher!("render", CMDRender => EntryRender); + +#[derive(Serialize, StructuralData, Groupped)] +struct Info { + name: String, + age: i32, +} + +#[chain] +fn handle_render(args: EntryRender) -> Next { + let name = args.inner.first().cloned().unwrap_or_default(); + let age = args.inner.get(1).and_then(|s| s.parse().ok()).unwrap_or(0); + Info { name, age }.to_render() +} + +#[renderer] +fn render_info(info: Info) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "{} is {} years old", info.name, info.age).ok(); + r +} +@@@ +@@@fn main() { +@@@ let mut program = ThisProgram::new(); +@@@ program.with_setup(StructuralRendererSetup); +@@@ program.with_dispatcher(CMDRender); +@@@ program.exec(); +@@@} +@@@gen_program!(); +``` + +Now `--json` outputs: + +```json +{ "name": "Bob", "age": 22 } +``` + +## Notes + +- Supported formats: JSON, YAML, TOML, RON (depends on enabled features) +- `StructuralRendererSetup` registers global params like `--json`, `--yaml`, `--toml`, `--ron` + +> [!NOTE] +> Each type still needs an **empty Renderer**, otherwise that type **is not considered renderable** + +See [example-structural-renderer](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-structural-renderer). + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/concepts/.name b/docs/pages/concepts/.name new file mode 100644 index 0000000..efb7494 --- /dev/null +++ b/docs/pages/concepts/.name @@ -0,0 +1 @@ +Core Concepts diff --git a/docs/pages/concepts/1-the-pipeline.md b/docs/pages/concepts/1-the-pipeline.md new file mode 100644 index 0000000..e73379d --- /dev/null +++ b/docs/pages/concepts/1-the-pipeline.md @@ -0,0 +1,129 @@ +<h1 align="center">The Pipeline</h1> +<p align="center"> + How Mingling executes commands, step by step +</p> + +Mingling splits command handling into three independent phases: Dispatcher → Chain → Renderer. This doc covers the actual execution logic — what happens at each step from user input to final output. + +## Full Flow + +```mermaid +graph TD + A["program.exec_and_exit()"] --> B["Hook: pre_dispatch"] + B --> C["Dispatch<br/>match cmd → Entry"] + C --> D["Hook: post_dispatch"] + D --> E{"user_context.help?"} + E -->|"true"| F["render_help<br/>skip to help rendering"] + E -->|"false"| G{"has_chain?"} + G -->|"yes"| H["Hook: pre_chain"] + H --> I["do_chain<br/>run business logic"] + I --> J{"ChainProcess?"} + J -->|"Ok(any, Renderer)"| K["Hook: pre_render →<br/>render → post_render"] + J -->|"Ok(any, Chain)"| G + J -->|"Err"| L["finish"] + G -->|"no"| M{"has_renderer?"} + M -->|"yes"| K + M -->|"no"| N["build_renderer_not_found"] + N --> G + K --> O["Hook: finish → return RenderResult"] + L --> O + F --> O +``` + +## Phase Breakdown + +### 1. Dispatch + +`exec_with_args` first calls `dispatch_args_dynamic` or `dispatch_args_trie` (depending on whether the `dispatch_tree` feature is enabled), matching user input against registered Dispatchers. + +The matching rule is **prefix matching** on space-separated tokens — the longest match wins. For example, if both `remote.add` and `remote` are registered, input `remote add origin` will match `remote.add`. + +```mermaid +graph LR + Input["user input"] --> M{"match Dispatcher"} + M -->|"matched"| E["call dispatcher.begin(args)<br/>return wrapped Entry"] + M -->|"no match"| NF["build_dispatcher_not_found<br/>generate ErrorDispatcherNotFound"] +``` + +On a match, `dispatcher.begin(args)` is called, returning `ChainProcess::Ok((AnyOutput, _))` — the Entry type wrapping the user's input params. + +If no Dispatcher matches, `ErrorDispatcherNotFound` is generated (wrapping the full input), which a Renderer can later handle to display "Command not found". + +### 2. Help Shortcut + +Before entering the main loop, `program.user_context.help` is checked. If `true` (set by `HelpFlagSetup` in `BasicProgramSetup` when `--help` is parsed), `render_help` is called directly, skipping the entire pipeline. + +### 3. Chain Main Loop + +This is the core scheduling logic. Each iteration checks the current `AnyOutput`: + +1. **Has a Chain** → execute `C::do_chain(current)` + - Returns `(AnyOutput, Renderer)` → exit loop, go to rendering + - Returns `(AnyOutput, Chain)` → continue loop, pass result to next Chain + - Returns `Err` → terminate + +2. **No Chain, but has a Renderer** → render directly + +3. **Neither** → generate `renderer_not_found`, then loop again (the newly generated type might have a Renderer) + +```mermaid +graph TD + Start["current AnyOutput"] --> C{"has_chain?"} + C -->|"yes"| Chain["do_chain"] + Chain -->|"returns (any, Chain)"| C + Chain -->|"returns (any, Renderer)"| Render["render"] + Chain -->|"Err"| Exit["exit"] + C -->|"no"| R{"has_renderer?"} + R -->|"yes"| Render + R -->|"no"| N["build_renderer_not_found<br/>try again"] + N --> C +``` + +### 4. Render + +The rendering phase calls `C::render(any, &mut render_result)`, which finds the matching `#[renderer]` function via `member_id` and writes the result into `RenderResult`. If `structural_renderer` is enabled, the result is also serialized to JSON/YAML (etc.) based on `program.structural_renderer_name`. + +### 5. Exit + +Sets `exit_code`, triggers the `finish` hook, and returns `RenderResult`. + +> [!TIP] +> This runtime dispatch code is driven by the enums generated by `gen_program!()` and the `ProgramCollect` implementation. +> +> At compile time only the type-to-Chain / Renderer / Help / Completion mapping is generated; actual matching and routing happens at runtime. + +## How This Is Different from Direct Function Calls + +This pipeline helps avoid writing code like this: + +```rust +@@@ struct Config; +@@@ impl Config { fn read() -> Self { Config } } +@@@ fn main() { +@@@ let json = true; +// read config +let mut config = Config::read(); + +// run operation +let Ok(result) = operation(&mut config) else { + panic!("error handling"); +}; + +// render result +if json { + print_json(); +} else { + println!("success!"); +} +@@@ } +@@@ fn operation(config: &mut Config) -> Result<(),()> { Ok(()) } +@@@ fn print_json() {} +``` + +Mingling's pipeline separates **cmd matching**, **business logic**, and **output rendering** into three independent slots, each responsible for one thing. + +More importantly, through hooks and the `AnyOutput` mechanism, the pipeline lets cross-cutting concerns (logging, auth, exit codes) be inserted non-invasively — no pollution of your business code. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/concepts/2-resource.md b/docs/pages/concepts/2-resource.md new file mode 100644 index 0000000..ad7ee16 --- /dev/null +++ b/docs/pages/concepts/2-resource.md @@ -0,0 +1,60 @@ +<h1 align="center">Resource System</h1> +<p align="center"> + How Mingling Manages Global State +</p> + +CLI programs often need to share global things—config files, database connections, counters, the current working directory. + +In vanilla Rust you might reach for `OnceCell` or `lazy_static`. In Mingling there's a unified mechanism: the **resource system**. + +## What is a Resource? + +A resource is data shared across multiple Chains and Renderers. + +You just define a type, register it with the Program, and declare it in your function signature—the framework handles injection and lifecycle management for you. + +## Core Mechanism: ResourceMarker + +Any type that implements both `Default + Clone` can automatically become a resource. The framework implements the `ResourceMarker` trait for it, giving it: + +- **`res_clone()`** — when multiple Chains access it concurrently, the framework can clone to avoid lock contention +- **`res_default()`** — provides a fallback value when the resource hasn't been registered + +If you need finer lifecycle control, you can use `LazyRes<T>`. It lets the resource be initialized on first access and can run a callback on drop (e.g., saving state to disk before exit). + +## Why Not Global Variables? + +The traditional approach with statics creates implicit dependencies—you can't tell from the function signature what global state it uses. Mingling's resource injection makes **dependencies explicit**: + +- Whatever resources a function needs go in its parameter list +- `&T` means read-only access, `&mut T` means mutable +- Callers can see the function's side effects at a glance + +For example: + +```rust +@@@ use mingling::res::ResExitCode; +@@@ pack!(ErrorFileNotFound = ()); +#[chain] +fn handle_error_file_not_found( + error: ErrorFileNotFound, + ec: &mut ResExitCode // the signature reveals the side effect! +) { + ec.exit_code = 2; // modifying the exit code here +} +``` + +## Resources and Setup + +Resources are typically registered with the Program in two ways: + +1. **Direct registration** — calling `program.with_resource(...)` in `main` +2. **Via Setup** — using built-in Setups like `DirectoryEnvironmentSetup` to batch-register resources (e.g., `ResCurrentDir`, `ResHomeDir`) + +A Setup is a higher-level abstraction than a resource—one Setup can register multiple resources and do other initialization work. + +See the [Program Assembly](./pages/8-setup-and-resources) chapter in the tutorial for more details. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/concepts/3-any-output.md b/docs/pages/concepts/3-any-output.md new file mode 100644 index 0000000..f780377 --- /dev/null +++ b/docs/pages/concepts/3-any-output.md @@ -0,0 +1,73 @@ +<h1 align="center">AnyOutput Mechanism</h1> +<p align="center"> + How AnyOutput and ChainProcess work +</p> + +What data is passed between the Dispatcher → Chain → Renderer stages? + +A Chain's output could be a successful result, an error, or something that still needs to go to the next Chain—these types are all different. How does the pipeline route them to the right place without knowing the concrete types at compile time? + +## AnyOutput: Type Erasure + Group Tag + +Mingling's solution is to **erase all types into the same wrapper**, then use an **enum tag** to distinguish them: + +``` +AnyOutput<G> +├── inner: Box<dyn Any + Send> ← the real data, type erased +├── type_id: TypeId ← runtime type ID, for safe downcast +└── member_id: G ← enum tag, marks "who this is" +``` + +Here `G` is the program enum generated by `gen_program!()` (i.e., `ThisProgram` as you know it). + +Each type annotated with `pack!` or `#[derive(Groupped)]` is assigned to one variant of this enum. + +## ChainProcess: Data + Routing + +On top of `AnyOutput`, `ChainProcess<G>` adds **routing info**: + +``` +ChainProcess<G> +├── Ok(AnyOutput<G>, NextProcess) ← carries data, tells the dispatcher where to go next +│ ├── NextProcess::Chain ← "not done yet, pass to the next Chain" +│ └── NextProcess::Renderer ← "got a result, show it to the user" +└── Err(ChainProcessError) ← "something went wrong, abort" +``` + +This is why a Chain function returns `ChainProcess` instead of raw data—it bundles **"where to go next"** and **"the data"** together. + +The dispatcher reads `NextProcess` to decide whether to continue the loop or exit to rendering. + +## Groupped: Who Is Who + +How does the dispatcher know whether an `AnyOutput` holds a `ResultName` or an `ErrorUserBlocked`? The answer is the `Groupped` trait: + +``` +trait Groupped<G> { + fn member_id() -> G; +} +``` + +When you use `pack!(ResultName = String)`, the macro automatically implements `Groupped` for `ResultName`, and `member_id()` returns the corresponding enum variant. The dispatcher looks at `member_id` and finds the matching Chain or Renderer. + +`to_chain()` and `to_render()` are essentially convenience methods on `AnyOutput` that construct `ChainProcess::Ok(any, Chain)` and `ChainProcess::Ok(any, Renderer)` respectively. + +## How Dispatching Works + +At runtime, the main loop does this: + +1. Check the current `AnyOutput`'s `member_id` +2. Look up whether this variant has a Chain → if yes, execute it, get a new `AnyOutput` and `NextProcess` +3. If `NextProcess` is `Chain` → go back to step 1 +4. If `NextProcess` is `Renderer` → exit the loop, render + +This mechanism ensures **type safety**: the dispatch code generated by `gen_program!()` only does `restore` (converting from `Box<dyn Any>` back to a concrete type) inside the matching `member_id` branch, so it's impossible to unwrap a `ResultName`'s data as if it were `ErrorUserBlocked`. + +> [!TIP] +> In day-to-day dev, you don't need to manually touch `AnyOutput` or `ChainProcess`. +> +> Macros like `pack!`, `#[chain]`, and `#[renderer]` handle all the wrapping and unwrapping for you. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/concepts/4-program-collect.md b/docs/pages/concepts/4-program-collect.md new file mode 100644 index 0000000..a24f115 --- /dev/null +++ b/docs/pages/concepts/4-program-collect.md @@ -0,0 +1,52 @@ +<h1 align="center">About ProgramCollect</h1> +<p align="center"> + Understand how gen_program!() builds a program +</p> + +Every Mingling program ends with a `gen_program!()` call. Behind the scenes, it does three things to scaffold the entire program. + +## The three tasks of gen_program!() + +### 1. Generate an enum + +Scans the current module for all types marked with `pack!`, `#[chain]`, `#[renderer]` and similar macros, then generates an enum variant for each type. + +This enum is the type of `G` in `AnyOutput<G>` — the scheduler uses enum variants to distinguish different data flowing through the pipeline. + +### 2. Generate a ProgramCollect impl + +`ProgramCollect` is a trait that defines the mapping of **"which type each enum variant corresponds to and who handles it"**: + +- **`do_chain`** — calls the corresponding `#[chain]` function by `member_id`, returns a new `AnyOutput` and `NextProcess` +- **`render`** — calls the corresponding `#[renderer]` function by `member_id`, writes to `RenderResult` +- **`render_help`** — calls the corresponding `#[help]` function by `member_id` +- **`has_chain` / `has_renderer`** — checks whether a variant has a corresponding handler +- **`build_dispatcher_not_found` / `build_renderer_not_found` / `build_empty_result`** — three built-in fallback types for edge cases + +This mapping is resolved at runtime via enum matching — only the enum and match branches are generated at compile time; actual function calls happen at runtime. + +### 3. Generate ThisProgram + +Generates the `ThisProgram` type alias, pointing to `Program<GeneratedEnum>`. That's why you can write `ThisProgram::new()` directly in `main` — it's the complete type of your whole program. + +--- + +## Differences under `pathf` and `dispatch_tree` + +The above describes the default behavior, which changes when specific features are enabled: + +### 1. `dispatch_tree` feature + +The Dispatcher no longer uses `Vec<Box<dyn Dispatcher>>` for linear matching. Instead, the subcommand structure is built as a prefix tree (Trie) at compile time. + +Matching complexity drops from `O(n)` to `O(k)` — where `k` is input length, independent of the number of commands. + +### 2. `pathf` feature (Module Pathfinder) + +By default, all macro-marked types must be in the same module for `gen_program!()` to collect them. + +With `pathf` enabled, the compiler automatically scans all sub-modules at compile time, finds all macro-marked types, and generates full module path references — types defined in deep sub-modules don't need a manual `use`. + +<p align="center" style="font-size: 0.85em; color: gray;"> + Written by @Weicao-CatilGrass +</p> diff --git a/docs/pages/other/naming_rule.md b/docs/pages/other/naming_rule.md index 21d7947..2ede61f 100644 --- a/docs/pages/other/naming_rule.md +++ b/docs/pages/other/naming_rule.md @@ -193,14 +193,18 @@ fn handle_state_operation_remotes(state: StateOperationRemotes, db: &ResDatabase // Result rendering #[renderer] -fn render_remote_added(result: ResultRemoteAdded) { - r_println!("Remote added: {}", result.inner); +fn render_remote_added(result: ResultRemoteAdded) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Remote added: {}", result.inner).ok(); + r } // Error rendering #[renderer] -fn render_error_repository_not_found(err: ErrorRepositoryNotFound) { - r_println!("Error: remote '{}' not found", err.inner); +fn render_error_repository_not_found(err: ErrorRepositoryNotFound) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Error: remote '{}' not found", err.inner).ok(); + r } ``` diff --git a/docs/res/guide.txt b/docs/res/guide.txt index 0582f87..657c7ca 100644 --- a/docs/res/guide.txt +++ b/docs/res/guide.txt @@ -5,5 +5,5 @@ │ > cargo add mingling │ │ │ │ Or add this to your Cargo.toml │ - │ > mingling = "0.2.0" │ + │ > mingling = "0.3.0" │ └────────────────────────────────────┘ diff --git a/examples/example-argument-parse/Cargo.lock b/examples/example-argument-parse/Cargo.lock index a424789..696dcb6 100644 --- a/examples/example-argument-parse/Cargo.lock +++ b/examples/example-argument-parse/Cargo.lock @@ -11,13 +11,13 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -26,14 +26,14 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", ] [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/examples/example-argument-parse/src/main.rs b/examples/example-argument-parse/src/main.rs index 4929b7c..59499a2 100644 --- a/examples/example-argument-parse/src/main.rs +++ b/examples/example-argument-parse/src/main.rs @@ -19,6 +19,7 @@ //! ``` use mingling::{macros::route, prelude::*}; +use std::io::Write; dispatcher!("transfer", CMDTransfer => EntryTransfer); dispatcher!("strict-transfer", CMDStrictTransfer => EntryStrictTransfer); @@ -71,20 +72,26 @@ fn handle_strict_transfer_parse(args: EntryStrictTransfer) -> Next { /// Renders the parsed transfer result (file/dir, size, name). #[renderer] -fn render_result_file(result: ResultFile) { +fn render_result_file(result: ResultFile) -> RenderResult { let (is_dir, size, name) = result.into(); - r_println!( + let mut result = RenderResult::new(); + writeln!( + result, "{}: {} ({})", if is_dir { "dir" } else { "file" }, name, size ) + .ok(); + result } /// Renders the error when no name is provided. #[renderer] -fn render_error_no_name_provided(_: ErrorNoNameProvided) { - r_println!("Error: name is not provided") +fn render_error_no_name_provided(_: ErrorNoNameProvided) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Error: name is not provided").ok(); + result } gen_program!(); diff --git a/examples/example-async-support/Cargo.lock b/examples/example-async-support/Cargo.lock index 3e23e92..83ce343 100644 --- a/examples/example-async-support/Cargo.lock +++ b/examples/example-async-support/Cargo.lock @@ -12,13 +12,13 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -27,14 +27,14 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", ] [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/examples/example-async-support/src/main.rs b/examples/example-async-support/src/main.rs index 847bfea..ac85aaf 100644 --- a/examples/example-async-support/src/main.rs +++ b/examples/example-async-support/src/main.rs @@ -24,6 +24,7 @@ //! ``` use mingling::{hook::ProgramHook, prelude::*}; +use std::io::Write; #[tokio::main] async fn main() { @@ -55,8 +56,10 @@ pub async fn handle_download(args: EntryDownload) -> Next { /// Renders the downloaded file name. #[renderer] // But renderers cannot use the `async` keyword -pub fn render_downloaded(result: ResultDownloaded) { - r_println!("\"{}\" downloaded.", *result); +pub fn render_downloaded(result: ResultDownloaded) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "\"{}\" downloaded.", *result).ok(); + render_result } // --------- IMPORTANT --------- diff --git a/examples/example-basic/Cargo.lock b/examples/example-basic/Cargo.lock index 6d22a16..90b8e4a 100644 --- a/examples/example-basic/Cargo.lock +++ b/examples/example-basic/Cargo.lock @@ -11,13 +11,13 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -25,14 +25,14 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", ] [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/examples/example-basic/src/main.rs b/examples/example-basic/src/main.rs index 3d94a4b..7fc0bce 100644 --- a/examples/example-basic/src/main.rs +++ b/examples/example-basic/src/main.rs @@ -14,6 +14,7 @@ // Import commonly used Mingling modules use mingling::prelude::*; +use std::io::Write; // Define the `greet` subcommand // _____________________________ subcmd name, can be nested (e.g. "remote.add" "remote.rm") @@ -60,8 +61,10 @@ fn handle_greet(args: EntryGreet) -> Next { // Define renderer `render_name`, used to render `ResultName` /// Renders the greeting message with the provided name. #[renderer] -fn render_name(name: ResultName) { - r_println!("Hello, {}!", *name); +fn render_name(name: ResultName) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "Hello, {}!", *name).ok(); + render_result } // Note: This macro generates the program entry point. diff --git a/examples/example-clap-binding/Cargo.lock b/examples/example-clap-binding/Cargo.lock index 63c5940..b747c40 100644 --- a/examples/example-clap-binding/Cargo.lock +++ b/examples/example-clap-binding/Cargo.lock @@ -120,13 +120,13 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -134,14 +134,14 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", ] [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/examples/example-clap-binding/src/main.rs b/examples/example-clap-binding/src/main.rs index 19d794b..d99f8d1 100644 --- a/examples/example-clap-binding/src/main.rs +++ b/examples/example-clap-binding/src/main.rs @@ -38,7 +38,8 @@ //! For more information, try '--help'. //! ``` -use mingling::{macros::dispatcher_clap, prelude::*, setup::BasicProgramSetup, Groupped}; +use mingling::{Groupped, macros::dispatcher_clap, prelude::*, setup::BasicProgramSetup}; +use std::io::Write; fn main() { let mut program = ThisProgram::new(); @@ -89,24 +90,29 @@ pub struct EntryGreet { /// Renders the greet output with optional repetition. #[renderer] -fn render_greet(greet: EntryGreet) { +fn render_greet(greet: EntryGreet) -> RenderResult { let name = greet.name; let count = greet.repeat.max(0) as usize; - r_print!("Hello, "); + let mut render_result = RenderResult::default(); + write!(render_result, "Hello, ").ok(); for i in 0..count { - r_print!("{name}"); + write!(render_result, "{name}").ok(); if i < count - 1 { - r_print!(", "); + write!(render_result, ", ").ok(); } } - r_println!("!"); + writeln!(render_result, "!").ok(); + render_result } /// Renders the error message when greet argument parsing fails. #[renderer] -fn render_greet_parse_failed(err: ErrorGreetParsed) { - r_println!("{}", *err); +// renderers can return a RenderResult instead of using r_println! +pub fn render_greet_parse_failed(err: ErrorGreetParsed) -> RenderResult { + let mut render_result = RenderResult::default(); + writeln!(render_result, "{}", *err).ok(); + render_result } gen_program!(); diff --git a/examples/example-combine-pathf-dispatch-tree/Cargo.lock b/examples/example-combine-pathf-dispatch-tree/Cargo.lock index 8b1f186..f91e03f 100644 --- a/examples/example-combine-pathf-dispatch-tree/Cargo.lock +++ b/examples/example-combine-pathf-dispatch-tree/Cargo.lock @@ -11,13 +11,13 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -25,7 +25,7 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "mingling_pathf", @@ -33,7 +33,7 @@ dependencies = [ [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", @@ -43,7 +43,7 @@ dependencies = [ [[package]] name = "mingling_pathf" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/examples/example-combine-pathf-dispatch-tree/src/sub/mod.rs b/examples/example-combine-pathf-dispatch-tree/src/sub/mod.rs index e0b7743..5ab0ece 100644 --- a/examples/example-combine-pathf-dispatch-tree/src/sub/mod.rs +++ b/examples/example-combine-pathf-dispatch-tree/src/sub/mod.rs @@ -1,5 +1,6 @@ use crate::Next; -use mingling::{macros::r_println, prelude::*}; +use mingling::prelude::*; +use std::io::Write; dispatcher!("hello"); @@ -17,6 +18,8 @@ pub fn handle_my(args: EntryHello) -> Next { } #[renderer] -pub fn render_my(msg: ResultMessage) { - r_println!("Hello, {}!", *msg); +pub fn render_my(msg: ResultMessage) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "Hello, {}!", *msg).ok(); + render_result } diff --git a/examples/example-completion/Cargo.lock b/examples/example-completion/Cargo.lock index 0da9702..568bec5 100644 --- a/examples/example-completion/Cargo.lock +++ b/examples/example-completion/Cargo.lock @@ -16,12 +16,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" [[package]] +name = "just_fmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" + +[[package]] name = "just_template" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb99a3c1dee7299c57b26ef927f15535da0fbc93d6deb1d1114cae1337be4fb" dependencies = [ - "just_fmt", + "just_fmt 0.1.2", "just_template_macros", ] @@ -38,7 +44,7 @@ dependencies = [ [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -47,17 +53,17 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", "just_template", ] [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", "proc-macro2", "quote", "syn", diff --git a/examples/example-completion/src/main.rs b/examples/example-completion/src/main.rs index 0c64d03..0159807 100644 --- a/examples/example-completion/src/main.rs +++ b/examples/example-completion/src/main.rs @@ -45,6 +45,7 @@ //! ``` use mingling::{macros::suggest, prelude::*, ShellContext, Suggest}; +use std::io::Write; fn main() { let mut program = ThisProgram::new(); @@ -118,13 +119,15 @@ fn handle_greet(args: EntryGreet) -> Next { /// Renders the greeting with the result name and repeat count. #[renderer] -fn render_name(result: ResultName) { +fn render_name(result: ResultName) -> RenderResult { let (repeat, name) = result.inner; + let mut render_result = RenderResult::new(); let mut parts = Vec::with_capacity(repeat as usize); for _ in 0..repeat { parts.push(name.clone()); } - r_println!("Hello, {}!", parts.join(", ")); + writeln!(render_result, "Hello, {}!", parts.join(", ")).ok(); + render_result } gen_program!(); diff --git a/examples/example-custom-pickable/Cargo.lock b/examples/example-custom-pickable/Cargo.lock index 08b3a6f..cb2d3ac 100644 --- a/examples/example-custom-pickable/Cargo.lock +++ b/examples/example-custom-pickable/Cargo.lock @@ -11,13 +11,13 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -26,14 +26,14 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", ] [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/examples/example-custom-pickable/src/main.rs b/examples/example-custom-pickable/src/main.rs index 015cc62..d203815 100644 --- a/examples/example-custom-pickable/src/main.rs +++ b/examples/example-custom-pickable/src/main.rs @@ -15,6 +15,7 @@ //! ``` use mingling::{macros::route, parser::Pickable, prelude::*, Groupped}; +use std::io::Write; // Define types that can be recognized by Mingling // ________________________ `Pickable` trait needs to implement Default @@ -52,14 +53,18 @@ fn handle_connect(prev: EntryConnect) -> Next { /// Renders the connected address. #[renderer] -fn render_address(addr: Address) { - r_println!("Connected to \"{}\"", addr.to_string()); +pub fn render_address(addr: Address) -> RenderResult { + let mut render_result = RenderResult::new(); + write!(render_result, "Connected to \"{}\"", addr).ok(); + render_result } /// Renders the error message when address parsing fails. #[renderer] -fn render_error_parse_address_failed(_: ErrorParseAddressFailed) { - r_println!("Failed to parse address"); +pub fn render_error_parse_address_failed(_: ErrorParseAddressFailed) -> RenderResult { + let mut render_result = RenderResult::new(); + write!(render_result, "Failed to parse address").ok(); + render_result } gen_program!(); diff --git a/examples/example-dispatch-tree/Cargo.lock b/examples/example-dispatch-tree/Cargo.lock index 305ea70..5751ecd 100644 --- a/examples/example-dispatch-tree/Cargo.lock +++ b/examples/example-dispatch-tree/Cargo.lock @@ -11,13 +11,13 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -25,14 +25,14 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", ] [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/examples/example-dispatch-tree/src/main.rs b/examples/example-dispatch-tree/src/main.rs index e087b1e..9f76f15 100644 --- a/examples/example-dispatch-tree/src/main.rs +++ b/examples/example-dispatch-tree/src/main.rs @@ -21,6 +21,7 @@ //! use mingling::prelude::*; +use std::io::Write; // --------- IMPORTANT --------- // You have a large number of subcommands @@ -53,8 +54,10 @@ fn main() { /// Renders the confirmation message for the `cmd5` command. #[renderer] -fn render_cmd5(_: Entry5) { - r_println!("It's works!"); +fn render_cmd5(_: Entry5) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "It's works!").ok(); + render_result } gen_program!(); diff --git a/examples/example-enum-tag/Cargo.lock b/examples/example-enum-tag/Cargo.lock index 3cb4258..211719f 100644 --- a/examples/example-enum-tag/Cargo.lock +++ b/examples/example-enum-tag/Cargo.lock @@ -16,12 +16,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" [[package]] +name = "just_fmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" + +[[package]] name = "just_template" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb99a3c1dee7299c57b26ef927f15535da0fbc93d6deb1d1114cae1337be4fb" dependencies = [ - "just_fmt", + "just_fmt 0.1.2", "just_template_macros", ] @@ -38,7 +44,7 @@ dependencies = [ [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -47,17 +53,17 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", "just_template", ] [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", "proc-macro2", "quote", "syn", diff --git a/examples/example-enum-tag/src/main.rs b/examples/example-enum-tag/src/main.rs index ad9db82..01c7767 100644 --- a/examples/example-enum-tag/src/main.rs +++ b/examples/example-enum-tag/src/main.rs @@ -19,6 +19,7 @@ use mingling::{ macros::suggest_enum, parser::PickableEnum, prelude::*, EnumTag, Groupped, ShellContext, Suggest, }; +use std::io::Write; // Define the enum and derive the EnumTag trait // ________ adds metadata to the enum, enabling it to: @@ -83,10 +84,11 @@ fn handle_language_selection(args: EntryLanguageSelection) -> Next { /// Renders the selected programming language with its name and description. #[renderer] -fn render_programming_language(lang: ProgrammingLanguages) { - // You can use `enum_info()` to get the name and description of the current enum +pub fn render_programming_language(lang: ProgrammingLanguages) -> RenderResult { + let mut render_result = RenderResult::new(); let (name, desc) = lang.enum_info(); - r_println!("Selected: {} ({})", name, desc) + writeln!(render_result, "Selected: {} ({})", name, desc).ok(); + render_result } #[completion(EntryLanguageSelection)] diff --git a/examples/example-error-handling/Cargo.lock b/examples/example-error-handling/Cargo.lock index c9bddd8..d139c14 100644 --- a/examples/example-error-handling/Cargo.lock +++ b/examples/example-error-handling/Cargo.lock @@ -11,13 +11,13 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -25,14 +25,14 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", ] [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/examples/example-error-handling/src/main.rs b/examples/example-error-handling/src/main.rs index ae79b97..de9792d 100644 --- a/examples/example-error-handling/src/main.rs +++ b/examples/example-error-handling/src/main.rs @@ -21,6 +21,7 @@ //! ``` use mingling::prelude::*; +use std::io::Write; // In Mingling, instead of using ? to propagate errors upward, // errors are treated as branches that continue execution. @@ -61,36 +62,47 @@ fn handle_hello(args: EntryHello) -> Next { /// Renders a successful greeting with the given name. #[renderer] -fn render_result_name(name: ResultName) { - r_println!("Hello, {}", *name); +fn render_result_name(name: ResultName) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "Hello, {}", *name).ok(); + render_result } /// Renders the error when no name is provided. #[renderer] -fn render_error_no_name_provided(_: ErrorNoNameProvided) { - // Prompt when no name is provided - r_println!("No name provided"); +fn render_error_no_name_provided(_: ErrorNoNameProvided) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "No name provided").ok(); + render_result } /// Renders the error when the name is already taken. #[renderer] -fn render_error_name_not_available(_: ErrorNameNotAvailable) { - // Prompt when name is already taken - r_println!("Name not available"); +fn render_error_name_not_available(_: ErrorNameNotAvailable) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "Name not available").ok(); + render_result } /// Renders the error when the name exceeds the maximum length. #[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); +fn render_error_name_too_long(len: ErrorNameTooLong) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "Name too long: {} > 10", *len).ok(); + render_result } /// Renders the error when the dispatcher (subcommand) is not found. #[renderer] -fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) { - // Prompt when command is not found, showing the input command - r_println!("Command not found: \"{}\"", err.inner.join(" ")); +fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!( + render_result, + "Command not found: \"{}\"", + err.inner.join(" ") + ) + .ok(); + render_result } gen_program!(); diff --git a/examples/example-exitcode/Cargo.lock b/examples/example-exitcode/Cargo.lock index 96bc223..df8b935 100644 --- a/examples/example-exitcode/Cargo.lock +++ b/examples/example-exitcode/Cargo.lock @@ -11,13 +11,13 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -25,14 +25,14 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", ] [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/examples/example-exitcode/src/main.rs b/examples/example-exitcode/src/main.rs index b0b7467..6b22eae 100644 --- a/examples/example-exitcode/src/main.rs +++ b/examples/example-exitcode/src/main.rs @@ -20,6 +20,7 @@ use mingling::{ res::ResExitCode, setup::{BasicProgramSetup, ExitCodeSetup}, }; +use std::io::Write; fn main() { let mut program = ThisProgram::new(); @@ -52,25 +53,32 @@ fn handle_hello(args: EntryHello) -> Next { /// Renders a successful greeting with the given name. #[renderer] -fn render_result_name(name: ResultName) { - r_println!("Hello, {}", *name); +fn render_result_name(name: ResultName) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Hello, {}", *name).ok(); + result } #[help] -fn help_hello(_p: EntryHello, ec: &mut ResExitCode) { - r_println!("Usage: hello <NAME>"); +fn help_hello(_p: EntryHello, ec: &mut ResExitCode) -> RenderResult { + let mut result = RenderResult::new(); + writeln!(result, "Usage: hello <NAME>").ok(); ec.exit_code = 2; + result } // Define renderer, render error message _______________ Inject exit code resource // / /// Renders the error when no name is provided | #[renderer] // vvvvvvvvvvvvvvvv -fn render_error_no_name_provided(_: ErrorNoNameProvided, ec: &mut ResExitCode) { +fn render_error_no_name_provided(_: ErrorNoNameProvided, ec: &mut ResExitCode) -> RenderResult { ec.exit_code = 1; + let mut result = RenderResult::new(); + // Prompt when no name is provided - r_println!("No name provided (with exit code 1)"); + writeln!(result, "No name provided (with exit code 1)").ok(); + result } gen_program!(); diff --git a/examples/example-help/Cargo.lock b/examples/example-help/Cargo.lock index 325f3ca..7a88e8e 100644 --- a/examples/example-help/Cargo.lock +++ b/examples/example-help/Cargo.lock @@ -11,13 +11,13 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -25,14 +25,14 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", ] [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/examples/example-help/src/main.rs b/examples/example-help/src/main.rs index 9567c49..3282683 100644 --- a/examples/example-help/src/main.rs +++ b/examples/example-help/src/main.rs @@ -14,14 +14,17 @@ //! ``` use mingling::{macros::help, prelude::*, setup::BasicProgramSetup}; +use std::io::Write; 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 help_greet(_prev: EntryGreet) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "Usage: greet <NAME>").ok(); + render_result } fn main() { diff --git a/examples/example-hook/Cargo.lock b/examples/example-hook/Cargo.lock index af0f614..cce863a 100644 --- a/examples/example-hook/Cargo.lock +++ b/examples/example-hook/Cargo.lock @@ -11,13 +11,13 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -25,14 +25,14 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", ] [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/examples/example-hook/src/main.rs b/examples/example-hook/src/main.rs index d9a8fd1..41928ca 100644 --- a/examples/example-hook/src/main.rs +++ b/examples/example-hook/src/main.rs @@ -24,6 +24,7 @@ use mingling::{ hook::{ProgramControlUnit, ProgramHook}, prelude::*, }; +use std::io::Write; dispatcher!("greet", CMDGreet => EntryGreet); @@ -67,9 +68,12 @@ fn handle_greet(args: EntryGreet) -> Next { } /// Renders the greeting message with the provided name. +/// Renders the greeting message with the provided name. #[renderer] -fn render_name(name: ResultName) { - r_println!("Hello, {}!", *name); +fn render_name(name: ResultName) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "Hello, {}!", *name).ok(); + render_result } gen_program!(); diff --git a/examples/example-implicit-dispatcher/Cargo.lock b/examples/example-implicit-dispatcher/Cargo.lock index 3ebc5e9..150104b 100644 --- a/examples/example-implicit-dispatcher/Cargo.lock +++ b/examples/example-implicit-dispatcher/Cargo.lock @@ -11,13 +11,13 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -25,14 +25,14 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", ] [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/examples/example-lazy-resources/Cargo.lock b/examples/example-lazy-resources/Cargo.lock index 33229d2..9ff79d2 100644 --- a/examples/example-lazy-resources/Cargo.lock +++ b/examples/example-lazy-resources/Cargo.lock @@ -11,13 +11,13 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -25,14 +25,14 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", ] [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/examples/example-lazy-resources/src/main.rs b/examples/example-lazy-resources/src/main.rs index 3533ae2..aaafb98 100644 --- a/examples/example-lazy-resources/src/main.rs +++ b/examples/example-lazy-resources/src/main.rs @@ -22,6 +22,7 @@ //! ``` use std::collections::BTreeMap; +use std::io::Write; use mingling::{LazyInit, LazyRes, prelude::*}; @@ -73,20 +74,25 @@ fn main() { // | _____________________ Use LazyRes<ResLargeData> // | / instead of ResLargeData #[renderer] // vvvv vvvvvvvvvvvvvvvvvvvvv -fn render_entry_show(_args: EntryShow, res: &mut LazyRes<ResLargeData>) { +fn render_entry_show(_args: EntryShow, res: &mut LazyRes<ResLargeData>) -> RenderResult { + let mut render_result = RenderResult::new(); + // _______ Initialization happens here // / // vvvvvvv let res = res.get_ref(); for (key, value) in &res.data { - r_println!("{}: {}", key, value); + writeln!(render_result, "{}: {}", key, value).ok(); } + render_result } // When not using LazyRes<ResLargeData>, it will not be initialized #[renderer] -fn render_entry_none(_args: EntryNone) { - r_println!("None"); +fn render_entry_none(_args: EntryNone) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "None").ok(); + render_result } gen_program!(); diff --git a/examples/example-outside-type/Cargo.lock b/examples/example-outside-type/Cargo.lock index b4ca61c..4c00bc8 100644 --- a/examples/example-outside-type/Cargo.lock +++ b/examples/example-outside-type/Cargo.lock @@ -11,13 +11,13 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -25,14 +25,14 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", ] [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/examples/example-outside-type/src/main.rs b/examples/example-outside-type/src/main.rs index 7cf4d40..925878a 100644 --- a/examples/example-outside-type/src/main.rs +++ b/examples/example-outside-type/src/main.rs @@ -19,6 +19,7 @@ //! ``` use mingling::{macros::group, prelude::*}; +use std::io::Write; use std::{io::ErrorKind::Other, num::ParseIntError}; dispatcher!("parse"); @@ -61,8 +62,10 @@ fn parse_number(args: EntryParse) -> Next { // _____________ Using std::num::ParseIntError as a chain input // / #[renderer] // vvvvvvvvvvvv -fn render_number(num: ParsedNumber) { - r_println!("Parsed number: {}", *num); +fn render_number(num: ParsedNumber) -> RenderResult { + let mut render_result = RenderResult::new(); + write!(render_result, "Parsed number: {}", *num).ok(); + render_result } /// Renderer for parse errors — using the outside `ParseIntError` type. @@ -70,16 +73,20 @@ fn render_number(num: ParsedNumber) { /// The `ParseIntError` type is registered via `group!` above, so it implements /// `Groupped<ThisProgram>` and can be used directly in a `#[renderer]` function. #[renderer] -fn render_parse_error(err: ParseIntError) { - r_println!("Parse error: {}", err); +fn render_parse_error(err: ParseIntError) -> RenderResult { + let mut render_result = RenderResult::new(); + write!(render_result, "Parse error: {}", err).ok(); + render_result } /// Renderer for IO errors — using `std::io::Error` registered as `ErrorIo`. // ________ Must use alias `ErrorIo` here, not bare `std::io::Error` // / #[renderer] // vvvvvvv -fn render_error_io(err: ErrorIo) { - r_println!("IO_ERROR: {}", err.to_string()); +fn render_error_io(err: ErrorIo) -> RenderResult { + let mut render_result = RenderResult::new(); + write!(render_result, "IO_ERROR: {}", err).ok(); + render_result } fn main() { diff --git a/examples/example-pack-err/Cargo.lock b/examples/example-pack-err/Cargo.lock index 258bdd7..26bad2b 100644 --- a/examples/example-pack-err/Cargo.lock +++ b/examples/example-pack-err/Cargo.lock @@ -18,9 +18,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "memchr" @@ -30,7 +30,7 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -39,7 +39,7 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "serde", @@ -48,7 +48,7 @@ dependencies = [ [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/examples/example-pack-err/src/main.rs b/examples/example-pack-err/src/main.rs index 8716333..5bf5066 100644 --- a/examples/example-pack-err/src/main.rs +++ b/examples/example-pack-err/src/main.rs @@ -28,6 +28,7 @@ use mingling::prelude::*; use mingling::setup::StructuralRendererSetup; +use std::io::Write; use std::path::PathBuf; dispatcher!("find", CMDFind => EntryFind); @@ -100,32 +101,42 @@ fn handle_find_structural(args: EntryFindStructural) -> Next { /// Renders the successful result with the found directory path. #[renderer] -fn render_result_path(path: ResultPath) { - r_println!("Found directory: {}", path.display()); +fn render_result_path(path: ResultPath) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "Found directory: {}", path.display()).ok(); + render_result } /// Renders the error when no search path is provided. #[renderer] -fn render_error_not_found(_: ErrorNotFound) { - r_println!("Search path not provided"); +fn render_error_not_found(_: ErrorNotFound) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "Search path not provided").ok(); + render_result } /// Renders the error when the given path is not a directory. #[renderer] -fn render_error_not_dir(err: ErrorNotDir) { - r_println!("Not a directory: {}", err.info.display()); +fn render_error_not_dir(err: ErrorNotDir) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "Not a directory: {}", err.info.display()).ok(); + render_result } /// Renders the structural error when no search path is provided. #[renderer] -fn render_error_not_found_structural(_: ErrorNotFoundStructural) { - r_println!("Search path not provided"); +fn render_error_not_found_structural(_: ErrorNotFoundStructural) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "Search path not provided").ok(); + render_result } /// Renders the structural error when the given path is not a directory. #[renderer] -fn render_error_not_dir_structural(err: ErrorNotDirStructural) { - r_println!("Not a directory: {}", err.info.display()); +fn render_error_not_dir_structural(err: ErrorNotDirStructural) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "Not a directory: {}", err.info.display()).ok(); + render_result } gen_program!(); diff --git a/examples/example-panic-unwind/Cargo.lock b/examples/example-panic-unwind/Cargo.lock index 7ec0af7..aafe504 100644 --- a/examples/example-panic-unwind/Cargo.lock +++ b/examples/example-panic-unwind/Cargo.lock @@ -11,13 +11,13 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -26,14 +26,14 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", ] [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/examples/example-panic-unwind/src/main.rs b/examples/example-panic-unwind/src/main.rs index ed032c5..a829158 100644 --- a/examples/example-panic-unwind/src/main.rs +++ b/examples/example-panic-unwind/src/main.rs @@ -16,6 +16,7 @@ //! ``` use mingling::{hook::ProgramHook, prelude::*}; +use std::io::Write; dispatcher!("panic", CMDPanic => EntryPanic); pack!(NotPanic = ()); @@ -51,9 +52,12 @@ fn handle_panic(prev: EntryPanic) -> Next { } /// Renders the message when no panic occurs. +/// Renders the message when no panic occurs. #[renderer] -fn render(_: NotPanic) { - r_println!("Program not panic"); +pub fn render(_: NotPanic) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "Program not panic").ok(); + render_result } gen_program!(); diff --git a/examples/example-pathfinder/Cargo.lock b/examples/example-pathfinder/Cargo.lock index 36840e7..3fe98bb 100644 --- a/examples/example-pathfinder/Cargo.lock +++ b/examples/example-pathfinder/Cargo.lock @@ -11,13 +11,13 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -25,7 +25,7 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "mingling_pathf", @@ -33,7 +33,7 @@ dependencies = [ [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", @@ -43,7 +43,7 @@ dependencies = [ [[package]] name = "mingling_pathf" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/examples/example-pathfinder/src/sub/mod.rs b/examples/example-pathfinder/src/sub/mod.rs index ef10a75..6d15930 100644 --- a/examples/example-pathfinder/src/sub/mod.rs +++ b/examples/example-pathfinder/src/sub/mod.rs @@ -1,5 +1,6 @@ -use mingling::prelude::*; use crate::Next; +use mingling::prelude::*; +use std::io::Write; dispatcher!("greet", CMDGreet => EntryGreet); pack!(ResultName = String); @@ -15,7 +16,11 @@ pub fn handle_greet(args: EntryGreet) -> Next { name } +/// Renders the name. #[renderer] -pub fn render_name(name: ResultName) { - r_println!("Hello, {}!", *name); +// But renderers cannot use the `async` keyword +pub fn render_name(name: ResultName) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "Hello, {}!", *name).ok(); + render_result } diff --git a/examples/example-repl-basic/Cargo.lock b/examples/example-repl-basic/Cargo.lock index b96ae7f..d405eaa 100644 --- a/examples/example-repl-basic/Cargo.lock +++ b/examples/example-repl-basic/Cargo.lock @@ -6,7 +6,7 @@ version = 4 name = "example-repl-basic" version = "0.1.0" dependencies = [ - "just_fmt", + "just_fmt 0.1.2", "mingling", ] @@ -17,8 +17,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" [[package]] -name = "mingling" +name = "just_fmt" version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" + +[[package]] +name = "mingling" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -27,16 +33,16 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", ] [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", "proc-macro2", "quote", "syn", diff --git a/examples/example-repl-basic/src/main.rs b/examples/example-repl-basic/src/main.rs index 8df8c22..d15319e 100644 --- a/examples/example-repl-basic/src/main.rs +++ b/examples/example-repl-basic/src/main.rs @@ -14,6 +14,7 @@ use mingling::{ setup::{BasicREPLOutputSetup, BasicREPLPromptSetup, BasicREPLReadlineSetup}, this, }; +use std::io::Write; use std::{env::current_dir, path::PathBuf}; // Resource to store the current directory @@ -136,10 +137,12 @@ fn handle_ls(_prev: EntryLs, current_dir: &ResCurrentDir) -> Next { /// Render ResultList data #[renderer] -fn render_list(list: ResultList) { +fn render_list(list: ResultList) -> RenderResult { + let mut render_result = RenderResult::new(); for item in list.inner { - r_println!("{}", item); + writeln!(render_result, "{}", item).ok(); } + render_result } // Handle exit command event @@ -161,15 +164,24 @@ fn handle_clear(_prev: EntryClear) { /// Handle path not found event #[renderer] -fn render_error_directory_not_exist(err: ErrorDirectoryNotExist) { - r_println!("Directory not found: {}", err.inner.display()) +fn render_error_directory_not_exist(err: ErrorDirectoryNotExist) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!( + render_result, + "Directory not found: {}", + err.inner.display() + ) + .ok(); + render_result } /// Handle dispatcher not found event /// Renders the error when a command is not found. #[renderer] -fn dispatcher_not_found(prev: ErrorDispatcherNotFound) { - r_println!("Command not found: \"{}\"", prev.join(", ")) +fn dispatcher_not_found(prev: ErrorDispatcherNotFound) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "Command not found: \"{}\"", prev.join(", ")).ok(); + render_result } gen_program!(); diff --git a/examples/example-resources/Cargo.lock b/examples/example-resources/Cargo.lock index b60d9f8..68d2138 100644 --- a/examples/example-resources/Cargo.lock +++ b/examples/example-resources/Cargo.lock @@ -11,13 +11,13 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -26,14 +26,14 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", ] [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/examples/example-resources/src/main.rs b/examples/example-resources/src/main.rs index 6c5bf84..b08ee26 100644 --- a/examples/example-resources/src/main.rs +++ b/examples/example-resources/src/main.rs @@ -14,9 +14,9 @@ //! Current directory: /home/alice/mingling/src //! ``` -use std::path::PathBuf; - use mingling::prelude::*; +use std::io::Write; +use std::path::PathBuf; // Create resource // ______________ Resource needs to @@ -58,8 +58,15 @@ fn render_modify_current(args: EntryModifyCurrent, current_dir: &mut ResCurrentD // / /// Renders the current directory path. | #[renderer] // vvvvvvvvvvvvvv -fn render_current(_: EntryCurrent, current_dir: &ResCurrentDir) { - r_println!("Current directory: {}", current_dir.current_dir.display()); +fn render_current(_: EntryCurrent, current_dir: &ResCurrentDir) -> RenderResult { + let mut render_result = RenderResult::new(); + write!( + render_result, + "Current directory: {}", + current_dir.current_dir.display() + ) + .ok(); + render_result } gen_program!(); diff --git a/examples/example-setup/Cargo.lock b/examples/example-setup/Cargo.lock index 5e0a341..c6b660b 100644 --- a/examples/example-setup/Cargo.lock +++ b/examples/example-setup/Cargo.lock @@ -11,13 +11,13 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -25,14 +25,14 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", ] [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/examples/example-structural-renderer/Cargo.lock b/examples/example-structural-renderer/Cargo.lock index cee5ae3..959c670 100644 --- a/examples/example-structural-renderer/Cargo.lock +++ b/examples/example-structural-renderer/Cargo.lock @@ -18,9 +18,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "memchr" @@ -30,7 +30,7 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -40,7 +40,7 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "serde", @@ -49,7 +49,7 @@ dependencies = [ [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/examples/example-structural-renderer/src/main.rs b/examples/example-structural-renderer/src/main.rs index 21077e7..13c4c84 100644 --- a/examples/example-structural-renderer/src/main.rs +++ b/examples/example-structural-renderer/src/main.rs @@ -18,8 +18,9 @@ //! ``` use mingling::prelude::*; -use mingling::{parser::Picker, setup::StructuralRendererSetup, StructuralData, Groupped}; +use mingling::{Groupped, StructuralData, parser::Picker, setup::StructuralRendererSetup}; use serde::Serialize; +use std::io::Write; dispatcher!("render", CMDRender => EntryRender); @@ -64,8 +65,10 @@ fn parse_render(prev: EntryRender) -> Next { /// Implement default renderer for when structural_renderer is not specified #[renderer] -fn render_info(prev: Info) { - r_println!("{} is {} years old", prev.name, prev.age); +fn render_info(prev: Info) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "{} is {} years old", prev.name, prev.age).ok(); + render_result } gen_program!(); diff --git a/examples/example-unit-test/Cargo.lock b/examples/example-unit-test/Cargo.lock index fa06882..2be0d51 100644 --- a/examples/example-unit-test/Cargo.lock +++ b/examples/example-unit-test/Cargo.lock @@ -11,13 +11,13 @@ dependencies = [ [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -25,14 +25,14 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", ] [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/examples/example-unit-test/src/main.rs b/examples/example-unit-test/src/main.rs index b89c629..b85ff01 100644 --- a/examples/example-unit-test/src/main.rs +++ b/examples/example-unit-test/src/main.rs @@ -7,6 +7,7 @@ //! ``` use mingling::prelude::*; +use std::io::Write; #[cfg(test)] mod tests { @@ -41,25 +42,25 @@ mod tests { #[test] fn test_render_result_name() { let r = render_result_name(ResultName::new("Peter".into())); - assert_eq!(r, "Hello, Peter!\n") + assert_eq!(r.to_string().as_str(), "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") + assert_eq!(r.to_string().as_str(), "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") + assert_eq!(r.to_string().as_str(), "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") + assert_eq!(r.to_string().as_str(), "Name too long: 17 > 10\n") } // --------- IMPORTANT --------- } @@ -93,32 +94,47 @@ fn handle_hello(args: EntryHello) -> Next { /// Renders a successful greeting with the given name. #[renderer] -fn render_result_name(name: ResultName) -> String { - r_println!("Hello, {}!", *name); +fn render_result_name(name: ResultName) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "Hello, {}!", *name).ok(); + render_result } /// Renders the error when no name is provided. #[renderer] -fn render_error_no_name_provided(_: ErrorNoNameProvided) -> String { - r_println!("No name provided"); +fn render_error_no_name_provided(_: ErrorNoNameProvided) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "No name provided").ok(); + render_result } /// Renders the error when the name is already taken. #[renderer] -fn render_error_name_not_available(_: ErrorNameNotAvailable) -> String { - r_println!("Name not available"); +fn render_error_name_not_available(_: ErrorNameNotAvailable) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "Name not available").ok(); + render_result } /// Renders the error when the name exceeds the maximum length. #[renderer] -fn render_error_name_too_long(len: ErrorNameTooLong) -> String { - r_println!("Name too long: {} > 10", *len); +fn render_error_name_too_long(len: ErrorNameTooLong) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "Name too long: {} > 10", *len).ok(); + render_result } /// Renders the error when the dispatcher (subcommand) is not found. #[renderer] -fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) { - r_println!("Command not found: \"{}\"", err.inner.join(" ")); +fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!( + render_result, + "Command not found: \"{}\"", + err.inner.join(" ") + ) + .ok(); + render_result } gen_program!(); diff --git a/examples/full-todolist/Cargo.lock b/examples/full-todolist/Cargo.lock index 3ce4651..f4f0000 100644 --- a/examples/full-todolist/Cargo.lock +++ b/examples/full-todolist/Cargo.lock @@ -19,9 +19,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "memchr" @@ -31,7 +31,7 @@ checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -41,7 +41,7 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "serde", @@ -50,7 +50,7 @@ dependencies = [ [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/examples/full-todolist/src/help.rs b/examples/full-todolist/src/help.rs index ab33176..2f8228a 100644 --- a/examples/full-todolist/src/help.rs +++ b/examples/full-todolist/src/help.rs @@ -1,15 +1,16 @@ //! This module provides help information for the `todolist` command line program -use mingling::macros::{help, r_println}; - use crate::{EntryAdd, EntryClean, EntryComplete, EntryList, ErrorDispatcherNotFound}; +use mingling::{RenderResult, macros::help}; +use std::io::Write; +/// Shows the global help message. #[help] -pub fn help_global(_p: ErrorDispatcherNotFound) { - r_println!( - "{}", - r" -Usage: todolist [command] [args] +pub fn help_global(_p: ErrorDispatcherNotFound) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!( + render_result, + r"Usage: todolist [command] [args] Commands: add -- Add a new task @@ -20,74 +21,78 @@ Commands: Args: -h, --help -- Show this help message -V, --version -- Show the version - -A, --all -- All tasks (Clean all / List all) - " - .trim() - ); + -A, --all -- All tasks (Clean all / List all)" + ) + .ok(); + render_result } +/// Shows help for the `add` command. #[help] -pub fn help_add(_p: EntryAdd) { - r_println!( - "{}", - r" -Usage: todolist add [task description] +pub fn help_add(_p: EntryAdd) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!( + render_result, + r"Usage: todolist add [task description] Add a new task to the todo list. Example: todolist add 'Buy groceries' - todolist add 'Finish Rust project' - " - .trim() - ); + todolist add 'Finish Rust project'" + ) + .ok(); + render_result } +/// Shows help for the `list` command. #[help] -pub fn help_list(_p: EntryList) { - r_println!( - "{}", - r" -Usage: todolist list +pub fn help_list(_p: EntryList) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!( + render_result, + r"Usage: todolist list List all tasks. Example: - todolist list - " - .trim() - ); + todolist list" + ) + .ok(); + render_result } +/// Shows help for the `complete` command. #[help] -pub fn help_complete(_p: EntryComplete) { - r_println!( - "{}", - r" -Usage: todolist complete [task_id] +pub fn help_complete(_p: EntryComplete) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!( + render_result, + r"Usage: todolist complete [task_id] Mark a task as complete by its ID. Example: todolist complete 1 - todolist complete 3 - " - .trim() - ); + todolist complete 3" + ) + .ok(); + render_result } +/// Shows help for the `clean` command. #[help] -pub fn help_clean(_p: EntryClean) { - r_println!( - "{}", - r" -Usage: todolist clean +pub fn help_clean(_p: EntryClean) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!( + render_result, + r"Usage: todolist clean Remove all completed tasks from the list. Example: - todolist clean - " - .trim() - ); + todolist clean" + ) + .ok(); + render_result } diff --git a/examples/full-todolist/src/main.rs b/examples/full-todolist/src/main.rs index e4c5aa6..0748832 100644 --- a/examples/full-todolist/src/main.rs +++ b/examples/full-todolist/src/main.rs @@ -7,12 +7,13 @@ //! > This is truly a cliché example, as common as `Hello World`! use mingling::{ + LazyInit, LazyRes, macros::route, prelude::*, res::ResExitCode, - setup::{ExitCodeSetup, StructuralRendererSetup, HelpFlagSetup}, - LazyInit, LazyRes, + setup::{ExitCodeSetup, HelpFlagSetup, StructuralRendererSetup}, }; +use std::io::Write; mod help; pub use help::*; @@ -160,38 +161,47 @@ fn handle_clean( } } +/// Renders error when no task description is provided. #[renderer] -fn render_error_no_task_description_provided( +pub fn render_error_no_task_description_provided( _err: ErrorNoTaskDescriptionProvided, // ExitCode ec: &mut ResExitCode, -) { - r_println!("No task description provided!"); - r_println!(""); - r_println!("Use `todolist add <desc>` to add a task"); +) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "No task description provided!").ok(); + writeln!(render_result).ok(); + writeln!(render_result, "Use `todolist add <desc>` to add a task").ok(); ec.exit_code = 1; + render_result } +/// Renders error when no index is provided. #[renderer] -fn render_error_no_index_provided( +pub fn render_error_no_index_provided( _err: ErrorNoIndexProvided, // ExitCode ec: &mut ResExitCode, -) { - r_println!("No index provided!"); - r_println!(""); - r_println!("Use `todolist list` to query indexes"); +) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "No index provided!").ok(); + writeln!(render_result).ok(); + writeln!(render_result, "Use `todolist list` to query indexes").ok(); ec.exit_code = 2; + render_result } +/// Renders error when index is out of bounds. #[renderer] -fn render_error_index_out_of_bounds( +pub fn render_error_index_out_of_bounds( _err: ErrorIndexOutOfBounds, // ExitCode ec: &mut ResExitCode, -) { - r_println!("Index out of bounds!"); +) -> RenderResult { + let mut render_result = RenderResult::new(); + writeln!(render_result, "Index out of bounds!").ok(); ec.exit_code = 3; + render_result } gen_program!(); diff --git a/examples/full-todolist/src/todolist.rs b/examples/full-todolist/src/todolist.rs index e447f5d..71338c3 100644 --- a/examples/full-todolist/src/todolist.rs +++ b/examples/full-todolist/src/todolist.rs @@ -1,10 +1,8 @@ //! Data structures, read and write logic for the todo list -use mingling::{ - macros::{r_println, renderer}, - Groupped, -}; +use mingling::{Groupped, RenderResult, macros::renderer}; use serde::{Deserialize, Serialize}; +use std::io::Write; use std::{env::current_dir, path::PathBuf}; use crate::ResProgramFlags; @@ -37,22 +35,30 @@ pub fn read_todo_list() -> ResTodoList { serde_json::from_reader(reader).unwrap_or_default() } +/// Renders the todo list. #[renderer] -pub fn render_res_todo_list(todo_list: ResTodoList, program_flags: &ResProgramFlags) { +pub fn render_res_todo_list( + todo_list: ResTodoList, + program_flags: &ResProgramFlags, +) -> RenderResult { + let mut render_result = RenderResult::new(); let mut idx = 0; - r_println!("TODO: "); + writeln!(render_result, "TODO: ").ok(); for item in &todo_list.items { if item.completed && !program_flags.all { idx += 1; continue; } - r_println!( + writeln!( + render_result, " {idx}. [{}] - \"{}\"", if item.completed { "x" } else { " " }, item.item - ); + ) + .ok(); idx += 1; } + render_result } pub fn write_todo_list(todo_list: ResTodoList) { @@ -757,8 +757,10 @@ fn handle_greet(args: EntryGreet) -> Next { } #[renderer] -fn render_greeting(greeting: ResultGreeting) { - r_println!("Hello, {}!", *greeting); +fn render_greet(result: ResultGreeting) -> RenderResult { + let mut r = RenderResult::new(); + writeln!(r, "Hello, {}!", *result).ok(); + r } gen_program!(); diff --git a/mingling/Cargo.toml b/mingling/Cargo.toml index be223fe..c3f9511 100644 --- a/mingling/Cargo.toml +++ b/mingling/Cargo.toml @@ -22,28 +22,35 @@ mingling = { path = ".", features = [ [package.metadata.docs.rs] features = [ + "core", + "macros", "builds", "structural_renderer", "repl", "comp", "parser", + "picker", "clap", "extra_macros", ] [features] +core = ["dep:mingling_core", "mingling_core/default"] +macros = ["dep:mingling_macros", "mingling_macros/default"] + nightly = ["mingling_core/nightly", "mingling_macros/nightly"] debug = ["mingling_core/debug"] async = ["mingling_core/async", "mingling_macros/async"] builds = ["mingling_core/builds"] -default = ["mingling_core/default", "mingling_macros/default"] +default = ["core", "macros"] clap = ["mingling_core/clap", "mingling_macros/clap"] dispatch_tree = ["mingling_core/dispatch_tree", "mingling_macros/dispatch_tree"] repl = ["mingling_core/repl", "mingling_macros/repl"] comp = ["mingling_core/comp", "mingling_macros/comp"] parser = ["dep:size"] +picker = ["dep:mingling_picker", "mingling_picker/mingling_support"] pathf = ["mingling_core/pathf", "mingling_macros/pathf"] structural_renderer = [ @@ -81,7 +88,8 @@ ron_serde_fmt = ["mingling_core/ron_serde_fmt"] extra_macros = ["mingling_macros/extra_macros"] [dependencies] -mingling_core.workspace = true -mingling_macros.workspace = true +mingling_core = { workspace = true, optional = true } +mingling_macros = { workspace = true, optional = true } +mingling_picker = { workspace = true, optional = true } serde = { workspace = true, optional = true } size = { version = "0.5", optional = true } diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs index 92b13a0..4699a50 100644 --- a/mingling/src/example_docs.rs +++ b/mingling/src/example_docs.rs @@ -39,6 +39,7 @@ /// Source code (./src/main.rs) /// ```ignore /// use mingling::{macros::route, prelude::*}; +/// use std::io::Write; /// /// dispatcher!("transfer", CMDTransfer => EntryTransfer); /// dispatcher!("strict-transfer", CMDStrictTransfer => EntryStrictTransfer); @@ -91,20 +92,26 @@ /// /// /// Renders the parsed transfer result (file/dir, size, name). /// #[renderer] -/// fn render_result_file(result: ResultFile) { +/// fn render_result_file(result: ResultFile) -> RenderResult { /// let (is_dir, size, name) = result.into(); -/// r_println!( +/// let mut result = RenderResult::new(); +/// writeln!( +/// result, /// "{}: {} ({})", /// if is_dir { "dir" } else { "file" }, /// name, /// size /// ) +/// .ok(); +/// result /// } /// /// /// Renders the error when no name is provided. /// #[renderer] -/// fn render_error_no_name_provided(_: ErrorNoNameProvided) { -/// r_println!("Error: name is not provided") +/// fn render_error_no_name_provided(_: ErrorNoNameProvided) -> RenderResult { +/// let mut result = RenderResult::new(); +/// writeln!(result, "Error: name is not provided").ok(); +/// result /// } /// /// gen_program!(); @@ -166,6 +173,7 @@ pub mod example_argument_parse {} /// Source code (./src/main.rs) /// ```ignore /// use mingling::{hook::ProgramHook, prelude::*}; +/// use std::io::Write; /// /// #[tokio::main] /// async fn main() { @@ -197,8 +205,10 @@ pub mod example_argument_parse {} /// /// Renders the downloaded file name. /// #[renderer] /// // But renderers cannot use the `async` keyword -/// pub fn render_downloaded(result: ResultDownloaded) { -/// r_println!("\"{}\" downloaded.", *result); +/// pub fn render_downloaded(result: ResultDownloaded) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!(render_result, "\"{}\" downloaded.", *result).ok(); +/// render_result /// } /// // --------- IMPORTANT --------- /// @@ -241,6 +251,7 @@ pub mod example_async_support {} /// ```ignore /// // Import commonly used Mingling modules /// use mingling::prelude::*; +/// use std::io::Write; /// /// // Define the `greet` subcommand /// // _____________________________ subcmd name, can be nested (e.g. "remote.add" "remote.rm") @@ -287,8 +298,10 @@ pub mod example_async_support {} /// // Define renderer `render_name`, used to render `ResultName` /// /// Renders the greeting message with the provided name. /// #[renderer] -/// fn render_name(name: ResultName) { -/// r_println!("Hello, {}!", *name); +/// fn render_name(name: ResultName) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!(render_result, "Hello, {}!", *name).ok(); +/// render_result /// } /// /// // Note: This macro generates the program entry point. @@ -365,7 +378,8 @@ pub mod example_basic {} /// /// Source code (./src/main.rs) /// ```ignore -/// use mingling::{macros::dispatcher_clap, prelude::*, setup::BasicProgramSetup, Groupped}; +/// use mingling::{Groupped, macros::dispatcher_clap, prelude::*, setup::BasicProgramSetup}; +/// use std::io::Write; /// /// fn main() { /// let mut program = ThisProgram::new(); @@ -416,24 +430,29 @@ pub mod example_basic {} /// /// /// Renders the greet output with optional repetition. /// #[renderer] -/// fn render_greet(greet: EntryGreet) { +/// fn render_greet(greet: EntryGreet) -> RenderResult { /// let name = greet.name; /// let count = greet.repeat.max(0) as usize; /// -/// r_print!("Hello, "); +/// let mut render_result = RenderResult::default(); +/// write!(render_result, "Hello, ").ok(); /// for i in 0..count { -/// r_print!("{name}"); +/// write!(render_result, "{name}").ok(); /// if i < count - 1 { -/// r_print!(", "); +/// write!(render_result, ", ").ok(); /// } /// } -/// r_println!("!"); +/// writeln!(render_result, "!").ok(); +/// render_result /// } /// /// /// Renders the error message when greet argument parsing fails. /// #[renderer] -/// fn render_greet_parse_failed(err: ErrorGreetParsed) { -/// r_println!("{}", *err); +/// // renderers can return a RenderResult instead of using r_println! +/// pub fn render_greet_parse_failed(err: ErrorGreetParsed) -> RenderResult { +/// let mut render_result = RenderResult::default(); +/// writeln!(render_result, "{}", *err).ok(); +/// render_result /// } /// /// gen_program!(); @@ -584,6 +603,7 @@ pub mod example_combine_pathf_dispatch_tree {} /// Source code (./src/main.rs) /// ```ignore /// use mingling::{macros::suggest, prelude::*, ShellContext, Suggest}; +/// use std::io::Write; /// /// fn main() { /// let mut program = ThisProgram::new(); @@ -657,13 +677,15 @@ pub mod example_combine_pathf_dispatch_tree {} /// /// /// Renders the greeting with the result name and repeat count. /// #[renderer] -/// fn render_name(result: ResultName) { +/// fn render_name(result: ResultName) -> RenderResult { /// let (repeat, name) = result.inner; +/// let mut render_result = RenderResult::new(); /// let mut parts = Vec::with_capacity(repeat as usize); /// for _ in 0..repeat { /// parts.push(name.clone()); /// } -/// r_println!("Hello, {}!", parts.join(", ")); +/// writeln!(render_result, "Hello, {}!", parts.join(", ")).ok(); +/// render_result /// } /// /// gen_program!(); @@ -703,6 +725,7 @@ pub mod example_completion {} /// Source code (./src/main.rs) /// ```ignore /// use mingling::{macros::route, parser::Pickable, prelude::*, Groupped}; +/// use std::io::Write; /// /// // Define types that can be recognized by Mingling /// // ________________________ `Pickable` trait needs to implement Default @@ -740,14 +763,18 @@ pub mod example_completion {} /// /// /// Renders the connected address. /// #[renderer] -/// fn render_address(addr: Address) { -/// r_println!("Connected to \"{}\"", addr.to_string()); +/// pub fn render_address(addr: Address) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// write!(render_result, "Connected to \"{}\"", addr).ok(); +/// render_result /// } /// /// /// Renders the error message when address parsing fails. /// #[renderer] -/// fn render_error_parse_address_failed(_: ErrorParseAddressFailed) { -/// r_println!("Failed to parse address"); +/// pub fn render_error_parse_address_failed(_: ErrorParseAddressFailed) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// write!(render_result, "Failed to parse address").ok(); +/// render_result /// } /// /// gen_program!(); @@ -856,6 +883,7 @@ pub mod example_custom_pickable {} /// Source code (./src/main.rs) /// ```ignore /// use mingling::prelude::*; +/// use std::io::Write; /// /// // --------- IMPORTANT --------- /// // You have a large number of subcommands @@ -888,8 +916,10 @@ pub mod example_custom_pickable {} /// /// /// Renders the confirmation message for the `cmd5` command. /// #[renderer] -/// fn render_cmd5(_: Entry5) { -/// r_println!("It's works!"); +/// fn render_cmd5(_: Entry5) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!(render_result, "It's works!").ok(); +/// render_result /// } /// /// gen_program!(); @@ -936,6 +966,7 @@ pub mod example_dispatch_tree {} /// macros::suggest_enum, parser::PickableEnum, prelude::*, EnumTag, Groupped, ShellContext, /// Suggest, /// }; +/// use std::io::Write; /// /// // Define the enum and derive the EnumTag trait /// // ________ adds metadata to the enum, enabling it to: @@ -1000,10 +1031,11 @@ pub mod example_dispatch_tree {} /// /// /// Renders the selected programming language with its name and description. /// #[renderer] -/// fn render_programming_language(lang: ProgrammingLanguages) { -/// // You can use `enum_info()` to get the name and description of the current enum +/// pub fn render_programming_language(lang: ProgrammingLanguages) -> RenderResult { +/// let mut render_result = RenderResult::new(); /// let (name, desc) = lang.enum_info(); -/// r_println!("Selected: {} ({})", name, desc) +/// writeln!(render_result, "Selected: {} ({})", name, desc).ok(); +/// render_result /// } /// /// #[completion(EntryLanguageSelection)] @@ -1060,6 +1092,7 @@ pub mod example_enum_tag {} /// Source code (./src/main.rs) /// ```ignore /// use mingling::prelude::*; +/// use std::io::Write; /// /// // In Mingling, instead of using ? to propagate errors upward, /// // errors are treated as branches that continue execution. @@ -1100,36 +1133,47 @@ pub mod example_enum_tag {} /// /// /// Renders a successful greeting with the given name. /// #[renderer] -/// fn render_result_name(name: ResultName) { -/// r_println!("Hello, {}", *name); +/// fn render_result_name(name: ResultName) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!(render_result, "Hello, {}", *name).ok(); +/// render_result /// } /// /// /// Renders the error when no name is provided. /// #[renderer] -/// fn render_error_no_name_provided(_: ErrorNoNameProvided) { -/// // Prompt when no name is provided -/// r_println!("No name provided"); +/// fn render_error_no_name_provided(_: ErrorNoNameProvided) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!(render_result, "No name provided").ok(); +/// render_result /// } /// /// /// Renders the error when the name is already taken. /// #[renderer] -/// fn render_error_name_not_available(_: ErrorNameNotAvailable) { -/// // Prompt when name is already taken -/// r_println!("Name not available"); +/// fn render_error_name_not_available(_: ErrorNameNotAvailable) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!(render_result, "Name not available").ok(); +/// render_result /// } /// /// /// Renders the error when the name exceeds the maximum length. /// #[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); +/// fn render_error_name_too_long(len: ErrorNameTooLong) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!(render_result, "Name too long: {} > 10", *len).ok(); +/// render_result /// } /// /// /// Renders the error when the dispatcher (subcommand) is not found. /// #[renderer] -/// fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) { -/// // Prompt when command is not found, showing the input command -/// r_println!("Command not found: \"{}\"", err.inner.join(" ")); +/// fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!( +/// render_result, +/// "Command not found: \"{}\"", +/// err.inner.join(" ") +/// ) +/// .ok(); +/// render_result /// } /// /// gen_program!(); @@ -1178,6 +1222,7 @@ pub mod example_error_handling {} /// res::ResExitCode, /// setup::{BasicProgramSetup, ExitCodeSetup}, /// }; +/// use std::io::Write; /// /// fn main() { /// let mut program = ThisProgram::new(); @@ -1210,25 +1255,32 @@ pub mod example_error_handling {} /// /// /// Renders a successful greeting with the given name. /// #[renderer] -/// fn render_result_name(name: ResultName) { -/// r_println!("Hello, {}", *name); +/// fn render_result_name(name: ResultName) -> RenderResult { +/// let mut result = RenderResult::new(); +/// writeln!(result, "Hello, {}", *name).ok(); +/// result /// } /// /// #[help] -/// fn help_hello(_p: EntryHello, ec: &mut ResExitCode) { -/// r_println!("Usage: hello <NAME>"); +/// fn help_hello(_p: EntryHello, ec: &mut ResExitCode) -> RenderResult { +/// let mut result = RenderResult::new(); +/// writeln!(result, "Usage: hello <NAME>").ok(); /// ec.exit_code = 2; +/// result /// } /// /// // Define renderer, render error message _______________ Inject exit code resource /// // / /// /// Renders the error when no name is provided | /// #[renderer] // vvvvvvvvvvvvvvvv -/// fn render_error_no_name_provided(_: ErrorNoNameProvided, ec: &mut ResExitCode) { +/// fn render_error_no_name_provided(_: ErrorNoNameProvided, ec: &mut ResExitCode) -> RenderResult { /// ec.exit_code = 1; /// +/// let mut result = RenderResult::new(); +/// /// // Prompt when no name is provided -/// r_println!("No name provided (with exit code 1)"); +/// writeln!(result, "No name provided (with exit code 1)").ok(); +/// result /// } /// /// gen_program!(); @@ -1265,14 +1317,17 @@ pub mod example_exitcode {} /// Source code (./src/main.rs) /// ```ignore /// use mingling::{macros::help, prelude::*, setup::BasicProgramSetup}; +/// use std::io::Write; /// /// 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 help_greet(_prev: EntryGreet) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!(render_result, "Usage: greet <NAME>").ok(); +/// render_result /// } /// /// fn main() { @@ -1333,6 +1388,7 @@ pub mod example_help {} /// hook::{ProgramControlUnit, ProgramHook}, /// prelude::*, /// }; +/// use std::io::Write; /// /// dispatcher!("greet", CMDGreet => EntryGreet); /// @@ -1376,9 +1432,12 @@ pub mod example_help {} /// } /// /// /// Renders the greeting message with the provided name. +/// /// Renders the greeting message with the provided name. /// #[renderer] -/// fn render_name(name: ResultName) { -/// r_println!("Hello, {}!", *name); +/// fn render_name(name: ResultName) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!(render_result, "Hello, {}!", *name).ok(); +/// render_result /// } /// /// gen_program!(); @@ -1465,6 +1524,7 @@ pub mod example_implicit_dispatcher {} /// Source code (./src/main.rs) /// ```ignore /// use std::collections::BTreeMap; +/// use std::io::Write; /// /// use mingling::{LazyInit, LazyRes, prelude::*}; /// @@ -1516,20 +1576,25 @@ pub mod example_implicit_dispatcher {} /// // | _____________________ Use LazyRes<ResLargeData> /// // | / instead of ResLargeData /// #[renderer] // vvvv vvvvvvvvvvvvvvvvvvvvv -/// fn render_entry_show(_args: EntryShow, res: &mut LazyRes<ResLargeData>) { +/// fn render_entry_show(_args: EntryShow, res: &mut LazyRes<ResLargeData>) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// /// // _______ Initialization happens here /// // / /// // vvvvvvv /// let res = res.get_ref(); /// for (key, value) in &res.data { -/// r_println!("{}: {}", key, value); +/// writeln!(render_result, "{}: {}", key, value).ok(); /// } +/// render_result /// } /// /// // When not using LazyRes<ResLargeData>, it will not be initialized /// #[renderer] -/// fn render_entry_none(_args: EntryNone) { -/// r_println!("None"); +/// fn render_entry_none(_args: EntryNone) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!(render_result, "None").ok(); +/// render_result /// } /// /// gen_program!(); @@ -1574,6 +1639,7 @@ pub mod example_lazy_resources {} /// Source code (./src/main.rs) /// ```ignore /// use mingling::{macros::group, prelude::*}; +/// use std::io::Write; /// use std::{io::ErrorKind::Other, num::ParseIntError}; /// /// dispatcher!("parse"); @@ -1616,8 +1682,10 @@ pub mod example_lazy_resources {} /// // _____________ Using std::num::ParseIntError as a chain input /// // / /// #[renderer] // vvvvvvvvvvvv -/// fn render_number(num: ParsedNumber) { -/// r_println!("Parsed number: {}", *num); +/// fn render_number(num: ParsedNumber) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// write!(render_result, "Parsed number: {}", *num).ok(); +/// render_result /// } /// /// /// Renderer for parse errors — using the outside `ParseIntError` type. @@ -1625,16 +1693,20 @@ pub mod example_lazy_resources {} /// /// The `ParseIntError` type is registered via `group!` above, so it implements /// /// `Groupped<ThisProgram>` and can be used directly in a `#[renderer]` function. /// #[renderer] -/// fn render_parse_error(err: ParseIntError) { -/// r_println!("Parse error: {}", err); +/// fn render_parse_error(err: ParseIntError) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// write!(render_result, "Parse error: {}", err).ok(); +/// render_result /// } /// /// /// Renderer for IO errors — using `std::io::Error` registered as `ErrorIo`. /// // ________ Must use alias `ErrorIo` here, not bare `std::io::Error` /// // / /// #[renderer] // vvvvvvv -/// fn render_error_io(err: ErrorIo) { -/// r_println!("IO_ERROR: {}", err.to_string()); +/// fn render_error_io(err: ErrorIo) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// write!(render_result, "IO_ERROR: {}", err).ok(); +/// render_result /// } /// /// fn main() { @@ -1699,6 +1771,7 @@ pub mod example_outside_type {} /// ```ignore /// use mingling::prelude::*; /// use mingling::setup::StructuralRendererSetup; +/// use std::io::Write; /// use std::path::PathBuf; /// /// dispatcher!("find", CMDFind => EntryFind); @@ -1771,32 +1844,42 @@ pub mod example_outside_type {} /// /// /// Renders the successful result with the found directory path. /// #[renderer] -/// fn render_result_path(path: ResultPath) { -/// r_println!("Found directory: {}", path.display()); +/// fn render_result_path(path: ResultPath) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!(render_result, "Found directory: {}", path.display()).ok(); +/// render_result /// } /// /// /// Renders the error when no search path is provided. /// #[renderer] -/// fn render_error_not_found(_: ErrorNotFound) { -/// r_println!("Search path not provided"); +/// fn render_error_not_found(_: ErrorNotFound) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!(render_result, "Search path not provided").ok(); +/// render_result /// } /// /// /// Renders the error when the given path is not a directory. /// #[renderer] -/// fn render_error_not_dir(err: ErrorNotDir) { -/// r_println!("Not a directory: {}", err.info.display()); +/// fn render_error_not_dir(err: ErrorNotDir) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!(render_result, "Not a directory: {}", err.info.display()).ok(); +/// render_result /// } /// /// /// Renders the structural error when no search path is provided. /// #[renderer] -/// fn render_error_not_found_structural(_: ErrorNotFoundStructural) { -/// r_println!("Search path not provided"); +/// fn render_error_not_found_structural(_: ErrorNotFoundStructural) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!(render_result, "Search path not provided").ok(); +/// render_result /// } /// /// /// Renders the structural error when the given path is not a directory. /// #[renderer] -/// fn render_error_not_dir_structural(err: ErrorNotDirStructural) { -/// r_println!("Not a directory: {}", err.info.display()); +/// fn render_error_not_dir_structural(err: ErrorNotDirStructural) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!(render_result, "Not a directory: {}", err.info.display()).ok(); +/// render_result /// } /// /// gen_program!(); @@ -1853,6 +1936,7 @@ pub mod example_pack_err {} /// Source code (./src/main.rs) /// ```ignore /// use mingling::{hook::ProgramHook, prelude::*}; +/// use std::io::Write; /// /// dispatcher!("panic", CMDPanic => EntryPanic); /// pack!(NotPanic = ()); @@ -1888,9 +1972,12 @@ pub mod example_pack_err {} /// } /// /// /// Renders the message when no panic occurs. +/// /// Renders the message when no panic occurs. /// #[renderer] -/// fn render(_: NotPanic) { -/// r_println!("Program not panic"); +/// pub fn render(_: NotPanic) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!(render_result, "Program not panic").ok(); +/// render_result /// } /// /// gen_program!(); @@ -1994,6 +2081,7 @@ pub mod example_pathfinder {} /// setup::{BasicREPLOutputSetup, BasicREPLPromptSetup, BasicREPLReadlineSetup}, /// this, /// }; +/// use std::io::Write; /// use std::{env::current_dir, path::PathBuf}; /// /// // Resource to store the current directory @@ -2116,10 +2204,12 @@ pub mod example_pathfinder {} /// /// /// Render ResultList data /// #[renderer] -/// fn render_list(list: ResultList) { +/// fn render_list(list: ResultList) -> RenderResult { +/// let mut render_result = RenderResult::new(); /// for item in list.inner { -/// r_println!("{}", item); +/// writeln!(render_result, "{}", item).ok(); /// } +/// render_result /// } /// /// // Handle exit command event @@ -2141,15 +2231,24 @@ pub mod example_pathfinder {} /// /// /// Handle path not found event /// #[renderer] -/// fn render_error_directory_not_exist(err: ErrorDirectoryNotExist) { -/// r_println!("Directory not found: {}", err.inner.display()) +/// fn render_error_directory_not_exist(err: ErrorDirectoryNotExist) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!( +/// render_result, +/// "Directory not found: {}", +/// err.inner.display() +/// ) +/// .ok(); +/// render_result /// } /// /// /// Handle dispatcher not found event /// /// Renders the error when a command is not found. /// #[renderer] -/// fn dispatcher_not_found(prev: ErrorDispatcherNotFound) { -/// r_println!("Command not found: \"{}\"", prev.join(", ")) +/// fn dispatcher_not_found(prev: ErrorDispatcherNotFound) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!(render_result, "Command not found: \"{}\"", prev.join(", ")).ok(); +/// render_result /// } /// /// gen_program!(); @@ -2187,9 +2286,9 @@ pub mod example_repl_basic {} /// /// Source code (./src/main.rs) /// ```ignore -/// use std::path::PathBuf; -/// /// use mingling::prelude::*; +/// use std::io::Write; +/// use std::path::PathBuf; /// /// // Create resource /// // ______________ Resource needs to @@ -2231,8 +2330,15 @@ pub mod example_repl_basic {} /// // / /// /// Renders the current directory path. | /// #[renderer] // vvvvvvvvvvvvvv -/// fn render_current(_: EntryCurrent, current_dir: &ResCurrentDir) { -/// r_println!("Current directory: {}", current_dir.current_dir.display()); +/// fn render_current(_: EntryCurrent, current_dir: &ResCurrentDir) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// write!( +/// render_result, +/// "Current directory: {}", +/// current_dir.current_dir.display() +/// ) +/// .ok(); +/// render_result /// } /// /// gen_program!(); @@ -2330,8 +2436,9 @@ pub mod example_setup {} /// Source code (./src/main.rs) /// ```ignore /// use mingling::prelude::*; -/// use mingling::{parser::Picker, setup::StructuralRendererSetup, StructuralData, Groupped}; +/// use mingling::{Groupped, StructuralData, parser::Picker, setup::StructuralRendererSetup}; /// use serde::Serialize; +/// use std::io::Write; /// /// dispatcher!("render", CMDRender => EntryRender); /// @@ -2376,8 +2483,10 @@ pub mod example_setup {} /// /// /// Implement default renderer for when structural_renderer is not specified /// #[renderer] -/// fn render_info(prev: Info) { -/// r_println!("{} is {} years old", prev.name, prev.age); +/// fn render_info(prev: Info) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!(render_result, "{} is {} years old", prev.name, prev.age).ok(); +/// render_result /// } /// /// gen_program!(); @@ -2407,6 +2516,7 @@ pub mod example_structural_renderer {} /// Source code (./src/main.rs) /// ```ignore /// use mingling::prelude::*; +/// use std::io::Write; /// /// #[cfg(test)] /// mod tests { @@ -2441,25 +2551,25 @@ pub mod example_structural_renderer {} /// #[test] /// fn test_render_result_name() { /// let r = render_result_name(ResultName::new("Peter".into())); -/// assert_eq!(r, "Hello, Peter!\n") +/// assert_eq!(r.to_string().as_str(), "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") +/// assert_eq!(r.to_string().as_str(), "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") +/// assert_eq!(r.to_string().as_str(), "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") +/// assert_eq!(r.to_string().as_str(), "Name too long: 17 > 10\n") /// } /// // --------- IMPORTANT --------- /// } @@ -2493,32 +2603,47 @@ pub mod example_structural_renderer {} /// /// /// Renders a successful greeting with the given name. /// #[renderer] -/// fn render_result_name(name: ResultName) -> String { -/// r_println!("Hello, {}!", *name); +/// fn render_result_name(name: ResultName) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!(render_result, "Hello, {}!", *name).ok(); +/// render_result /// } /// /// /// Renders the error when no name is provided. /// #[renderer] -/// fn render_error_no_name_provided(_: ErrorNoNameProvided) -> String { -/// r_println!("No name provided"); +/// fn render_error_no_name_provided(_: ErrorNoNameProvided) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!(render_result, "No name provided").ok(); +/// render_result /// } /// /// /// Renders the error when the name is already taken. /// #[renderer] -/// fn render_error_name_not_available(_: ErrorNameNotAvailable) -> String { -/// r_println!("Name not available"); +/// fn render_error_name_not_available(_: ErrorNameNotAvailable) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!(render_result, "Name not available").ok(); +/// render_result /// } /// /// /// Renders the error when the name exceeds the maximum length. /// #[renderer] -/// fn render_error_name_too_long(len: ErrorNameTooLong) -> String { -/// r_println!("Name too long: {} > 10", *len); +/// fn render_error_name_too_long(len: ErrorNameTooLong) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!(render_result, "Name too long: {} > 10", *len).ok(); +/// render_result /// } /// /// /// Renders the error when the dispatcher (subcommand) is not found. /// #[renderer] -/// fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) { -/// r_println!("Command not found: \"{}\"", err.inner.join(" ")); +/// fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult { +/// let mut render_result = RenderResult::new(); +/// writeln!( +/// render_result, +/// "Command not found: \"{}\"", +/// err.inner.join(" ") +/// ) +/// .ok(); +/// render_result /// } /// /// gen_program!(); diff --git a/mingling/src/features.rs b/mingling/src/features.rs index 4d0c50b..cb474ae 100644 --- a/mingling/src/features.rs +++ b/mingling/src/features.rs @@ -53,6 +53,17 @@ pub const MINGLING_COMP: bool = false; #[cfg(feature = "comp")] #[allow(unused)] pub const MINGLING_COMP: bool = true; +/// Whether the `core` feature is enabled +/// Current: `disabled` +#[cfg(not(feature = "core"))] +#[allow(unused)] +pub const MINGLING_CORE: bool = false; + +/// Whether the `core` feature is enabled +/// Current: `enabled` +#[cfg(feature = "core")] +#[allow(unused)] +pub const MINGLING_CORE: bool = true; /// Whether the `debug` feature is enabled /// Current: `disabled` #[cfg(not(feature = "debug"))] @@ -108,6 +119,17 @@ pub const MINGLING_JSON_SERDE_FMT: bool = false; #[cfg(feature = "json_serde_fmt")] #[allow(unused)] pub const MINGLING_JSON_SERDE_FMT: bool = true; +/// Whether the `macros` feature is enabled +/// Current: `disabled` +#[cfg(not(feature = "macros"))] +#[allow(unused)] +pub const MINGLING_MACROS: bool = false; + +/// Whether the `macros` feature is enabled +/// Current: `enabled` +#[cfg(feature = "macros")] +#[allow(unused)] +pub const MINGLING_MACROS: bool = true; /// Whether the `nightly` feature is enabled /// Current: `disabled` #[cfg(not(feature = "nightly"))] @@ -141,6 +163,17 @@ pub const MINGLING_PATHF: bool = false; #[cfg(feature = "pathf")] #[allow(unused)] pub const MINGLING_PATHF: bool = true; +/// Whether the `picker` feature is enabled +/// Current: `disabled` +#[cfg(not(feature = "picker"))] +#[allow(unused)] +pub const MINGLING_PICKER: bool = false; + +/// Whether the `picker` feature is enabled +/// Current: `enabled` +#[cfg(feature = "picker")] +#[allow(unused)] +pub const MINGLING_PICKER: bool = true; /// Whether the `repl` feature is enabled /// Current: `disabled` #[cfg(not(feature = "repl"))] diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs index b4372dc..ba87f85 100644 --- a/mingling/src/lib.rs +++ b/mingling/src/lib.rs @@ -33,15 +33,19 @@ //! } //! //! #[renderer] -//! fn render_name(name: ResultName) { -//! r_println!("Hello, {}!", *name); +//! fn render_name(name: ResultName) -> RenderResult { +//! let mut result = RenderResult::default(); +//! result.println(&format!("Hello, {}!", *name)); +//! result //! } //! //! #[renderer] -//! fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) { +//! fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult { +//! let mut result = RenderResult::default(); //! if err.len() > 0 { -//! r_println!("Command not found: [{}]", err.join(" ")); +//! result.println(&format!("Command not found: [{}]", err.join(" "))); //! } +//! result //! } //! //! gen_program!(); @@ -65,21 +69,35 @@ //! # About Features //! All features of `Mingling` are opt-in. To learn what each feature provides, see [Features](feature/index.html) or [Helpdoc](https://mingling-rs.github.io/mingling/docs/doc.html#/pages/other/features) +#[cfg(feature = "core")] mod example_docs; // Re-export Core lib +#[cfg(feature = "core")] pub use mingling::*; + +#[cfg(feature = "core")] pub use mingling_core as mingling; -/// `Mingling` argument parser +/// `Mingling` argument parser (Built-in) #[cfg(feature = "parser")] pub mod parser; +/// `Mingling` argument parser (Picker2) +#[cfg(feature = "picker")] +pub mod picker { + pub use mingling_picker::*; + + pub mod parselib { + pub use mingling_picker::parselib::*; + } +} + /// Re-export of all macros from `mingling_macros`. /// /// This module re-exports all macros provided by the `mingling_macros` crate, -/// including `dispatcher!`, `chain!`, `renderer!`, `gen_program!`, `pack!`, -/// `r_print!`, `r_println!`, and many others. These macros form the core +/// including `dispatcher!`, `chain!`, `renderer!`, +/// `gen_program!`, `pack!`, and many others. These macros form the core /// building blocks of the Mingling framework. /// /// For detailed documentation, usage examples, and the full list of available @@ -87,6 +105,7 @@ pub mod parser; /// /// <https://docs.rs/mingling_macros/latest/mingling_macros/> #[allow(unused_imports)] +#[cfg(feature = "macros")] pub mod macros { /// `#[chain]` - Used to generate a struct implementing the `Chain` trait via a method pub use mingling_macros::chain; @@ -118,15 +137,15 @@ pub mod macros { pub use mingling_macros::node; /// `pack!(StateGreet = String)` - Used to create a wrapper type for use with `Chain` and `Renderer` pub use mingling_macros::pack; - /// `pack_structural!(StateGreet = String)` - Like `pack!` but also marks the type for structured output - #[cfg(feature = "structural_renderer")] - pub use mingling_macros::pack_structural; /// `pack_err!(ErrorUnknown)` - Used to create an error struct with automatic `name` field #[cfg(feature = "extra_macros")] pub use mingling_macros::pack_err; /// `pack_err_structural!(ErrorUnknown)` - Like `pack_err!` but also marks the type for structured output #[cfg(all(feature = "structural_renderer", feature = "extra_macros"))] pub use mingling_macros::pack_err_structural; + /// `pack_structural!(StateGreet = String)` - Like `pack!` but also marks the type for structured output + #[cfg(feature = "structural_renderer")] + pub use mingling_macros::pack_structural; #[cfg(feature = "comp")] #[doc(hidden)] pub use mingling_macros::program_comp_gen; @@ -137,10 +156,6 @@ pub mod macros { /// `#[program_setup]` - Used to generate program setup #[cfg(feature = "extra_macros")] pub use mingling_macros::program_setup; - /// `r_print!("{someting}")` - Used to print content within a `Renderer` context - pub use mingling_macros::r_print; - /// `r_println!("{someting}")` - Used to print content with a newline within a `Renderer` context - pub use mingling_macros::r_println; #[doc(hidden)] pub use mingling_macros::register_chain; #[doc(hidden)] @@ -162,12 +177,17 @@ pub mod macros { /// `suggest_enum!(EnumNames)` - Used to generate enum suggestions #[cfg(feature = "comp")] pub use mingling_macros::suggest_enum; + /// New Parser provided by the `picker` feature + #[cfg(feature = "picker")] + pub use mingling_picker::macros::*; } /// derive macro `EnumTag` +#[cfg(feature = "macros")] pub use mingling_macros::EnumTag; /// derive macro Groupped +#[cfg(feature = "macros")] pub use mingling_macros::Groupped; /// derive macro `StructuralData` — marks a type as supporting structured output @@ -175,10 +195,12 @@ pub use mingling_macros::Groupped; pub use mingling_macros::StructuralData; /// Example projects for `Mingling`, for learning how to use `Mingling` +#[cfg(feature = "core")] pub mod _mingling_examples { pub use crate::example_docs::*; } +#[cfg(feature = "core")] mod features; /// Module for checking which features are enabled at compile time. @@ -186,19 +208,23 @@ mod features; /// Each constant re-exported from this module corresponds to a Cargo feature flag. /// They can be used for conditional compilation or runtime branching based on /// feature availability. +#[cfg(feature = "core")] pub mod feature { include!("./features.rs"); } +#[cfg(feature = "core")] mod setups; /// Setups provided by Mingling, which can extend command-line programs. +#[cfg(feature = "core")] pub mod setup { pub use crate::setups::*; pub use mingling_core::setup::*; } /// Mutable global resources provided within Mingling +#[cfg(feature = "core")] pub mod res; /// The prelude module provides convenient re-exports of commonly used macros and traits. @@ -214,40 +240,55 @@ pub mod res; /// ``` pub mod prelude { /// Re-export of the `Groupped` derive macro for grouping types. + #[cfg(feature = "core")] pub use crate::Groupped; + /// Re-export of the `RenderResult` struct for outputting rendering result + #[cfg(feature = "core")] + pub use crate::RenderResult; /// Re-export of the `chain` macro for defining a chain of commands. + #[cfg(feature = "macros")] pub use crate::macros::chain; /// Re-export of the `dispatcher` macro for routing commands. + #[cfg(feature = "macros")] pub use crate::macros::dispatcher; /// Re-export of the `empty_result` macro for creating an empty result value for early return. - #[cfg(feature = "extra_macros")] + #[cfg(all(feature = "extra_macros", feature = "macros"))] pub use crate::macros::empty_result; /// Re-export of the `gen_program` macro for generating the program entry point. + #[cfg(feature = "macros")] pub use crate::macros::gen_program; /// Re-export of the `pack` macro for creating wrapper types. + #[cfg(feature = "macros")] pub use crate::macros::pack; - /// Like `pack!` but also marks the type for structured output - #[cfg(feature = "structural_renderer")] - pub use mingling_macros::pack_structural; /// Re-export of the `pack_err` macro for creating error types. - #[cfg(feature = "extra_macros")] + #[cfg(all(feature = "extra_macros", feature = "macros"))] pub use crate::macros::pack_err; - /// Like `pack_err!` but also marks the type for structured output - #[cfg(all(feature = "structural_renderer", feature = "extra_macros"))] - pub use mingling_macros::pack_err_structural; - /// Re-export of the `r_print` macro for printing within a renderer context. - pub use crate::macros::r_print; - /// Re-export of the `r_println` macro for printing with a newline within a renderer - /// context. - pub use crate::macros::r_println; /// Re-export of the `renderer` macro for defining renderer functions. + #[cfg(feature = "macros")] pub use crate::macros::renderer; + /// Like `pack_err!` but also marks the type for structured output + #[cfg(all( + feature = "macros", + feature = "structural_renderer", + feature = "extra_macros" + ))] + pub use mingling_macros::pack_err_structural; + /// Like `pack!` but also marks the type for structured output + #[cfg(all(feature = "macros", feature = "structural_renderer"))] + pub use mingling_macros::pack_structural; /// Re-export of the `completion` macro for generating completion entries. - #[cfg(feature = "comp")] + #[cfg(all(feature = "macros", feature = "comp"))] pub use crate::macros::completion; /// Re-export of the `AsPicker` trait for picker functionality. #[cfg(feature = "parser")] pub use crate::parser::AsPicker; + + #[cfg(feature = "picker")] + pub use mingling_picker::prelude::*; + + /// Used to enable the `writeln!` macro for `RenderResult` + #[cfg(feature = "core")] + pub use std::io::Write; } diff --git a/mingling/src/parser/picker.rs b/mingling/src/parser/picker.rs index 21ba9a6..601cd3f 100644 --- a/mingling/src/parser/picker.rs +++ b/mingling/src/parser/picker.rs @@ -722,6 +722,13 @@ impl_pick_with_route_next! { PickWithRoute9 PickWithRoute10 val_10 T1 val_1, T2 impl_pick_with_route_next! { PickWithRoute10 PickWithRoute11 val_11 T1 val_1, T2 val_2, T3 val_3, T4 val_4, T5 val_5, T6 val_6, T7 val_7, T8 val_8, T9 val_9, T10 val_10 } impl_pick_with_route_next! { PickWithRoute11 PickWithRoute12 val_12 T1 val_1, T2 val_2, T3 val_3, T4 val_4, T5 val_5, T6 val_6, T7 val_7, T8 val_8, T9 val_9, T10 val_10, T11 val_11 } +/// Trait for types that can be used with `Pickable` to extract enum values from command-line arguments. +/// +/// This trait combines `EnumTag` (for building an enum variant from a string name) and `Default` +/// (for providing a fallback value when the flag is not present). +/// +/// Types implementing this trait can be used with `Picker::pick`, `Picker::pick_or_route`, and +/// the chaining `.pick()` methods to extract and parse enum values from command-line arguments. pub trait PickableEnum: EnumTag + Default {} impl<T> Pickable for T diff --git a/mingling/src/parser/picker/bools.rs b/mingling/src/parser/picker/bools.rs index ede8812..0525c52 100644 --- a/mingling/src/parser/picker/bools.rs +++ b/mingling/src/parser/picker/bools.rs @@ -1,9 +1,15 @@ use crate::parser::Pickable; +/// Represents a boolean-like value with `Yes` and `No` variants. +/// +/// `Yes` can be parsed from command-line arguments using positive keywords such as `"y"` or `"yes"`, +/// and defaults to `No`. #[derive(Debug, Default)] #[repr(u8)] pub enum Yes { + /// The affirmative/positive variant. Yes, + /// The negative/default variant. #[default] No, } @@ -57,10 +63,16 @@ impl Pickable for Yes { } } +/// Represents a boolean-like value with `True` and `False` variants. +/// +/// `True` can be parsed from command-line arguments using positive keywords such as `"t"` or `"true"`, +/// and defaults to `False`. #[derive(Debug, Default)] #[repr(u8)] pub enum True { + /// The affirmative/positive variant. True, + /// The negative/default variant. #[default] False, } diff --git a/mingling/src/res/dirs/current_dir.rs b/mingling/src/res/dirs/current_dir.rs index 1de84e0..54d2b05 100644 --- a/mingling/src/res/dirs/current_dir.rs +++ b/mingling/src/res/dirs/current_dir.rs @@ -17,7 +17,7 @@ use std::{ /// a `Result`. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResCurrentDir { - cwd: PathBuf + cwd: PathBuf, } impl ResCurrentDir { @@ -27,13 +27,17 @@ impl ResCurrentDir { /// deleted or permissions are insufficient). Unlike the `Default` implementation, this /// method does not panic on failure. pub fn new() -> Result<Self, std::io::Error> { - Ok(Self { cwd: current_dir()? }) + Ok(Self { + cwd: current_dir()?, + }) } } impl Default for ResCurrentDir { fn default() -> Self { - Self { cwd: current_dir().unwrap() } + Self { + cwd: current_dir().unwrap(), + } } } @@ -45,7 +49,9 @@ impl From<PathBuf> for ResCurrentDir { impl From<&Path> for ResCurrentDir { fn from(path: &Path) -> Self { - Self { cwd: path.to_path_buf() } + Self { + cwd: path.to_path_buf(), + } } } diff --git a/mingling/src/res/dirs/current_exe.rs b/mingling/src/res/dirs/current_exe.rs index 051fcee..051f380 100644 --- a/mingling/src/res/dirs/current_exe.rs +++ b/mingling/src/res/dirs/current_exe.rs @@ -26,13 +26,17 @@ impl ResCurrentExe { /// filesystem is not available on Linux, or the process handle is invalid). /// Unlike the `Default` implementation, this method does not panic on failure. pub fn new() -> Result<Self, std::io::Error> { - Ok(Self { exe: current_exe()? }) + Ok(Self { + exe: current_exe()?, + }) } } impl Default for ResCurrentExe { fn default() -> Self { - Self { exe: current_exe().unwrap() } + Self { + exe: current_exe().unwrap(), + } } } @@ -44,7 +48,9 @@ impl From<PathBuf> for ResCurrentExe { impl From<&Path> for ResCurrentExe { fn from(path: &Path) -> Self { - Self { exe: path.to_path_buf() } + Self { + exe: path.to_path_buf(), + } } } diff --git a/mingling/src/res/dirs/home_dir.rs b/mingling/src/res/dirs/home_dir.rs index de2e5e7..fae3055 100644 --- a/mingling/src/res/dirs/home_dir.rs +++ b/mingling/src/res/dirs/home_dir.rs @@ -24,15 +24,18 @@ impl ResHomeDir { /// Returns `Err` if the home directory cannot be determined (e.g., the `HOME` or /// `USERPROFILE` environment variable is not set). pub fn new() -> Result<Self, std::io::Error> { - let home = home_dir_env() - .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "home directory not found"))?; + let home = home_dir_env().ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::NotFound, "home directory not found") + })?; Ok(Self { home }) } } impl Default for ResHomeDir { fn default() -> Self { - Self { home: home_dir_env().expect("home directory not found") } + Self { + home: home_dir_env().expect("home directory not found"), + } } } @@ -44,7 +47,9 @@ impl From<PathBuf> for ResHomeDir { impl From<&Path> for ResHomeDir { fn from(path: &Path) -> Self { - Self { home: path.to_path_buf() } + Self { + home: path.to_path_buf(), + } } } diff --git a/mingling/src/res/dirs/temp_dir.rs b/mingling/src/res/dirs/temp_dir.rs index f4f0dca..343d9c7 100644 --- a/mingling/src/res/dirs/temp_dir.rs +++ b/mingling/src/res/dirs/temp_dir.rs @@ -40,7 +40,9 @@ impl From<PathBuf> for ResTempDir { impl From<&Path> for ResTempDir { fn from(path: &Path) -> Self { - Self { tmp: path.to_path_buf() } + Self { + tmp: path.to_path_buf(), + } } } diff --git a/mingling/src/setups/dirs.rs b/mingling/src/setups/dirs.rs index a7f81fa..1f5e2d2 100644 --- a/mingling/src/setups/dirs.rs +++ b/mingling/src/setups/dirs.rs @@ -1,9 +1,6 @@ use std::marker::PhantomData; -use mingling_core::{ - ProgramCollect, - setup::ProgramSetup, -}; +use mingling_core::{ProgramCollect, setup::ProgramSetup}; use crate::res::{ResCurrentDir, ResCurrentExe, ResHomeDir, ResTempDir}; diff --git a/mingling_core/src/any.rs b/mingling_core/src/any.rs index b586f09..2680f43 100644 --- a/mingling_core/src/any.rs +++ b/mingling_core/src/any.rs @@ -1,5 +1,5 @@ -use crate::{Groupped, ProgramCollect}; use crate::error::ChainProcessError; +use crate::{Groupped, ProgramCollect}; #[doc(hidden)] pub mod group; @@ -136,7 +136,10 @@ impl<G> From<AnyOutput<G>> for ChainProcess<G> { } } -impl<G> From<()> for ChainProcess<G> where G: ProgramCollect<Enum = G> { +impl<G> From<()> for ChainProcess<G> +where + G: ProgramCollect<Enum = G>, +{ fn from(_v: ()) -> Self { G::build_empty_result().route_chain() } diff --git a/mingling_core/src/asset/global_resource.rs b/mingling_core/src/asset/global_resource.rs index 651655a..3d0af7b 100644 --- a/mingling_core/src/asset/global_resource.rs +++ b/mingling_core/src/asset/global_resource.rs @@ -80,9 +80,7 @@ where /// owned value. The caller must call [`__store_res`] to write back modifications. #[doc(hidden)] #[must_use] - pub fn __extract_res_mut<Res: 'static + Default + ResourceMarker + Send + Sync>( - &self, - ) -> Res { + pub fn __extract_res_mut<Res: 'static + Default + ResourceMarker + Send + Sync>(&self) -> Res { let Ok(mut guard) = self.resources.lock() else { return Res::res_default(); }; @@ -103,10 +101,7 @@ where /// /// Stores a modified resource value back into the global store. #[doc(hidden)] - pub fn __store_res<Res: 'static + Send + Sync + ResourceMarker>( - &self, - val: Res, - ) { + pub fn __store_res<Res: 'static + Send + Sync + ResourceMarker>(&self, val: Res) { if let Ok(mut guard) = self.resources.lock() { guard.insert(TypeId::of::<Res>(), Box::new(Arc::new(val))); } diff --git a/mingling_core/src/asset/help.rs b/mingling_core/src/asset/help.rs index ff2b3d4..b3742f2 100644 --- a/mingling_core/src/asset/help.rs +++ b/mingling_core/src/asset/help.rs @@ -6,5 +6,5 @@ pub trait HelpRequest { type Entry; /// Process the previous value and write the result into the provided [`RenderResult`](./struct.RenderResult.html) - fn render_help(p: Self::Entry, r: &mut RenderResult); + fn render_help(p: Self::Entry) -> RenderResult; } diff --git a/mingling_core/src/asset/renderer.rs b/mingling_core/src/asset/renderer.rs index 1d5a2c1..732f9b7 100644 --- a/mingling_core/src/asset/renderer.rs +++ b/mingling_core/src/asset/renderer.rs @@ -6,5 +6,5 @@ pub trait Renderer { type Previous; /// Process the previous value and write the result into the provided [`RenderResult`](./struct.RenderResult.html) - fn render(p: Self::Previous, r: &mut RenderResult); + fn render(p: Self::Previous) -> RenderResult; } diff --git a/mingling_core/src/program/collection.rs b/mingling_core/src/program/collection.rs index 044379c..d5aab4b 100644 --- a/mingling_core/src/program/collection.rs +++ b/mingling_core/src/program/collection.rs @@ -53,10 +53,10 @@ pub trait ProgramCollect { fn build_empty_result() -> AnyOutput<Self::Enum>; /// Render the input [`AnyOutput`](./struct.AnyOutput.html) - fn render(any: AnyOutput<Self::Enum>, r: &mut RenderResult); + fn render(any: AnyOutput<Self::Enum>) -> RenderResult; /// Render help for Entry - fn render_help(any: AnyOutput<Self::Enum>, r: &mut RenderResult); + fn render_help(any: AnyOutput<Self::Enum>) -> RenderResult; /// Find a matching chain to continue execution based on the input [AnyOutput](./struct.AnyOutput.html), returning a new [AnyOutput](./struct.AnyOutput.html) #[cfg(feature = "async")] diff --git a/mingling_core/src/program/collection/mock.rs b/mingling_core/src/program/collection/mock.rs index 568000a..9b2e7af 100644 --- a/mingling_core/src/program/collection/mock.rs +++ b/mingling_core/src/program/collection/mock.rs @@ -59,11 +59,11 @@ impl ProgramCollect for MockProgramCollect { unreachable!() } - fn render(_any: AnyOutput<Self::Enum>, _r: &mut RenderResult) { + fn render(_any: AnyOutput<Self::Enum>) -> RenderResult { unreachable!() } - fn render_help(_any: AnyOutput<Self::Enum>, _r: &mut RenderResult) { + fn render_help(_any: AnyOutput<Self::Enum>) -> RenderResult { unreachable!() } diff --git a/mingling_core/src/program/exec.rs b/mingling_core/src/program/exec.rs index d1983ed..f5bdc2b 100644 --- a/mingling_core/src/program/exec.rs +++ b/mingling_core/src/program/exec.rs @@ -461,19 +461,13 @@ pub(crate) fn handle_program_control<C: ProgramCollect<Enum = C>>( fn render<C: ProgramCollect<Enum = C>>(program: &Program<C>, any: AnyOutput<C>) -> RenderResult { #[cfg(not(feature = "structural_renderer"))] { - let mut render_result = RenderResult::default(); - C::render(any, &mut render_result); - render_result + C::render(any) } #[cfg(feature = "structural_renderer")] { #[allow(unreachable_patterns)] match program.structural_renderer_name { - super::StructuralRendererSetting::Disable => { - let mut render_result = RenderResult::default(); - C::render(any, &mut render_result); - render_result - } + super::StructuralRendererSetting::Disable => C::render(any), _ => C::structural_render(any, &program.structural_renderer_name).unwrap(), } } @@ -487,19 +481,13 @@ fn render_help<C: ProgramCollect<Enum = C>>( ) -> RenderResult { #[cfg(not(feature = "structural_renderer"))] { - let mut render_result = RenderResult::default(); - C::render_help(entry, &mut render_result); - render_result + C::render_help(entry) } #[cfg(feature = "structural_renderer")] { #[allow(unreachable_patterns)] match program.structural_renderer_name { - super::StructuralRendererSetting::Disable => { - let mut render_result = RenderResult::default(); - C::render_help(entry, &mut render_result); - render_result - } + super::StructuralRendererSetting::Disable => C::render_help(entry), _ => RenderResult::default(), } } diff --git a/mingling_core/src/program/hook.rs b/mingling_core/src/program/hook.rs index 630baae..56d8e0e 100644 --- a/mingling_core/src/program/hook.rs +++ b/mingling_core/src/program/hook.rs @@ -718,11 +718,11 @@ mod tests { unreachable!() } - fn render(_any: crate::AnyOutput<MockHookEnum>, _r: &mut crate::RenderResult) { + fn render(_any: crate::AnyOutput<MockHookEnum>) -> crate::RenderResult { unreachable!() } - fn render_help(_any: crate::AnyOutput<MockHookEnum>, _r: &mut crate::RenderResult) { + fn render_help(_any: crate::AnyOutput<MockHookEnum>) -> crate::RenderResult { unreachable!() } diff --git a/mingling_core/src/renderer.rs b/mingling_core/src/renderer.rs index 435518d..6e67431 100644 --- a/mingling_core/src/renderer.rs +++ b/mingling_core/src/renderer.rs @@ -1,3 +1,3 @@ +pub mod render_result; #[cfg(feature = "structural_renderer")] pub mod structural; -pub mod render_result; diff --git a/mingling_core/src/renderer/render_result.rs b/mingling_core/src/renderer/render_result.rs index 5ef3120..82a745c 100644 --- a/mingling_core/src/renderer/render_result.rs +++ b/mingling_core/src/renderer/render_result.rs @@ -52,6 +52,23 @@ impl From<&RenderResult> for String { } impl RenderResult { + /// Creates a new `RenderResult` with default values (empty text and exit code 0). + /// + /// Equivalent to `RenderResult::default()`. + /// + /// # Examples + /// + /// ``` + /// use mingling_core::RenderResult; + /// + /// let result = RenderResult::new(); + /// assert_eq!(result.exit_code, 0); + /// assert!(result.is_empty()); + /// ``` + pub fn new() -> Self { + Self::default() + } + /// Appends the given text to the rendered content. /// /// # Examples diff --git a/mingling_core/src/renderer/structural.rs b/mingling_core/src/renderer/structural.rs index 16ce471..30255aa 100644 --- a/mingling_core/src/renderer/structural.rs +++ b/mingling_core/src/renderer/structural.rs @@ -1,5 +1,6 @@ use crate::{ - StructuralRendererSetting, RenderResult, renderer::structural::error::StructuralRendererSerializeError, + RenderResult, StructuralRendererSetting, + renderer::structural::error::StructuralRendererSerializeError, }; use serde::Serialize; @@ -103,9 +104,7 @@ impl StructuralRenderer { data: &T, r: &mut RenderResult, ) -> Result<(), StructuralRendererSerializeError> { - let pretty_config = ron::ser::PrettyConfig::new() - .new_line("\n") - .indentor(" "); + let pretty_config = ron::ser::PrettyConfig::new().new_line("\n").indentor(" "); let ron_string = ron::ser::to_string_pretty(data, pretty_config) .map_err(|e| StructuralRendererSerializeError::new(e.to_string()))?; @@ -123,8 +122,8 @@ impl StructuralRenderer { data: &T, r: &mut RenderResult, ) -> Result<(), StructuralRendererSerializeError> { - let toml_string = - toml::to_string(data).map_err(|e| StructuralRendererSerializeError::new(e.to_string()))?; + let toml_string = toml::to_string(data) + .map_err(|e| StructuralRendererSerializeError::new(e.to_string()))?; r.print(&toml_string); Ok(()) } @@ -196,8 +195,11 @@ mod tests { #[test] fn test_render_to_json_pretty() { let mut r = RenderResult::default(); - let result = - StructuralRenderer::render(&test_data(), &StructuralRendererSetting::JsonPretty, &mut r); + let result = StructuralRenderer::render( + &test_data(), + &StructuralRendererSetting::JsonPretty, + &mut r, + ); assert!(result.is_ok()); let output: String = r.into(); // Pretty JSON has newlines @@ -261,7 +263,8 @@ mod tests { #[test] fn test_render_dispatches_json() { let mut r = RenderResult::default(); - let result = StructuralRenderer::render(&test_data(), &StructuralRendererSetting::Json, &mut r); + let result = + StructuralRenderer::render(&test_data(), &StructuralRendererSetting::Json, &mut r); assert!(result.is_ok()); assert!(!r.is_empty()); } diff --git a/mingling_core/tests/test-all/Cargo.lock b/mingling_core/tests/test-all/Cargo.lock index b92e13f..bb7b75b 100644 --- a/mingling_core/tests/test-all/Cargo.lock +++ b/mingling_core/tests/test-all/Cargo.lock @@ -68,12 +68,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" [[package]] +name = "just_fmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" + +[[package]] name = "just_template" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb99a3c1dee7299c57b26ef927f15535da0fbc93d6deb1d1114cae1337be4fb" dependencies = [ - "just_fmt", + "just_fmt 0.1.2", "just_template_macros", ] @@ -111,7 +117,7 @@ checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -121,9 +127,9 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", "just_template", "ron", "serde", @@ -134,9 +140,9 @@ dependencies = [ [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", "proc-macro2", "quote", "syn", diff --git a/mingling_core/tests/test-basic/Cargo.lock b/mingling_core/tests/test-basic/Cargo.lock index 212d194..5029bbf 100644 --- a/mingling_core/tests/test-basic/Cargo.lock +++ b/mingling_core/tests/test-basic/Cargo.lock @@ -4,13 +4,13 @@ version = 4 [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -18,14 +18,14 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", ] [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/mingling_core/tests/test-comp/Cargo.lock b/mingling_core/tests/test-comp/Cargo.lock index f339baf..175b65e 100644 --- a/mingling_core/tests/test-comp/Cargo.lock +++ b/mingling_core/tests/test-comp/Cargo.lock @@ -9,12 +9,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" [[package]] +name = "just_fmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" + +[[package]] name = "just_template" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb99a3c1dee7299c57b26ef927f15535da0fbc93d6deb1d1114cae1337be4fb" dependencies = [ - "just_fmt", + "just_fmt 0.1.2", "just_template_macros", ] @@ -31,7 +37,7 @@ dependencies = [ [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -39,17 +45,17 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", "just_template", ] [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", "proc-macro2", "quote", "syn", diff --git a/mingling_core/tests/test-dispatch-tree/Cargo.lock b/mingling_core/tests/test-dispatch-tree/Cargo.lock index 03b1e84..39d82c5 100644 --- a/mingling_core/tests/test-dispatch-tree/Cargo.lock +++ b/mingling_core/tests/test-dispatch-tree/Cargo.lock @@ -4,13 +4,13 @@ version = 4 [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -18,14 +18,14 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", ] [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/mingling_core/tests/test-repl/Cargo.lock b/mingling_core/tests/test-repl/Cargo.lock index 118681a..2b990f2 100644 --- a/mingling_core/tests/test-repl/Cargo.lock +++ b/mingling_core/tests/test-repl/Cargo.lock @@ -4,13 +4,13 @@ version = 4 [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -18,14 +18,14 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", ] [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/mingling_core/tests/test-structural-renderer/Cargo.lock b/mingling_core/tests/test-structural-renderer/Cargo.lock index 39077be..bf5291d 100644 --- a/mingling_core/tests/test-structural-renderer/Cargo.lock +++ b/mingling_core/tests/test-structural-renderer/Cargo.lock @@ -41,9 +41,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "just_fmt" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "memchr" @@ -53,7 +53,7 @@ checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "mingling" -version = "0.2.0" +version = "0.3.0" dependencies = [ "mingling_core", "mingling_macros", @@ -63,7 +63,7 @@ dependencies = [ [[package]] name = "mingling_core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "ron", @@ -75,7 +75,7 @@ dependencies = [ [[package]] name = "mingling_macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "just_fmt", "proc-macro2", diff --git a/mingling_macros/Cargo.toml b/mingling_macros/Cargo.toml index 66f654b..1191015 100644 --- a/mingling_macros/Cargo.toml +++ b/mingling_macros/Cargo.toml @@ -8,7 +8,7 @@ readme = "README.md" repository.workspace = true description = "Macros of the mingling library" keywords = ["cli", "macros", "procedural", "command-line", "framework"] -categories = ["command-line-interface", "development-tools::procedural-macros"] +categories = ["command-line-interface"] [lib] proc-macro = true diff --git a/mingling_macros/src/dispatcher.rs b/mingling_macros/src/dispatcher.rs index 36bdf31..2a7c850 100644 --- a/mingling_macros/src/dispatcher.rs +++ b/mingling_macros/src/dispatcher.rs @@ -217,7 +217,10 @@ pub fn register_dispatcher(input: TokenStream) -> TokenStream { } = syn::parse_macro_input!(input as RegisterDispatcherInput); let node_name_str = node_name.value(); - let static_name = format!("__internal_dispatcher_{}", snake_case!(node_name_str.clone())); + let static_name = format!( + "__internal_dispatcher_{}", + snake_case!(node_name_str.clone()) + ); let static_ident = Ident::new(&static_name, proc_macro2::Span::call_site()); // Register node info in the global collection at compile time diff --git a/mingling_macros/src/dispatcher_clap.rs b/mingling_macros/src/dispatcher_clap.rs index 5f06fb4..0945e31 100644 --- a/mingling_macros/src/dispatcher_clap.rs +++ b/mingling_macros/src/dispatcher_clap.rs @@ -144,7 +144,7 @@ pub fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> TokenStream Some(quote! { #[allow(non_snake_case)] #[::mingling::macros::help] - pub fn #help_fn_name(_prev: #struct_name) { + pub fn #help_fn_name(_prev: #struct_name) -> ::mingling::RenderResult { use std::io::Write; use clap::ColorChoice; @@ -154,11 +154,14 @@ pub fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> TokenStream let mut cmd = <#struct_name as ::clap::CommandFactory>::command() .color(ColorChoice::Always); let styled = cmd.render_help(); - write!(__renderer_inner_result, "{}", styled.ansi()).unwrap(); + let mut result = ::mingling::RenderResult::new(); + let _ = write!(result, "{}", styled.ansi()); + result } ::mingling::ClapHelpPrintBehaviour::PrintDirectly => { let mut command = <#struct_name as ::clap::CommandFactory>::command(); command.print_help().unwrap(); + ::mingling::RenderResult::new() } } } diff --git a/mingling_macros/src/help.rs b/mingling_macros/src/help.rs index 6d427b9..1903d07 100644 --- a/mingling_macros/src/help.rs +++ b/mingling_macros/src/help.rs @@ -1,22 +1,34 @@ use proc_macro::TokenStream; use quote::{ToTokens, quote}; use syn::spanned::Spanned; -use syn::{Ident, ItemFn, ReturnType, Signature, Type, TypePath, parse_macro_input}; +use syn::{Ident, ItemFn, ReturnType, Type, TypePath, parse_macro_input}; use crate::get_global_set; use crate::res_injection::{extract_args_info, generate_immut_resource_bindings}; -/// Validates the return type is () or empty -fn validate_return_type(sig: &Signature) -> syn::Result<()> { +/// Validates that the function returns `::mingling::RenderResult`. +fn validate_render_result_return(sig: &syn::Signature) -> syn::Result<()> { match &sig.output { ReturnType::Type(_, ty) => match &**ty { - Type::Tuple(tuple) if tuple.elems.is_empty() => Ok(()), + Type::Path(type_path) => { + let last_seg = type_path.path.segments.last().map(|s| s.ident.to_string()); + match last_seg.as_deref() { + Some("RenderResult") => Ok(()), + _ => Err(syn::Error::new( + ty.span(), + "Help function must return `RenderResult`", + )), + } + } _ => Err(syn::Error::new( ty.span(), - "Help function must return () or have no return type", + "Help function must return `RenderResult`", )), }, - ReturnType::Default => Ok(()), + ReturnType::Default => Err(syn::Error::new( + sig.span(), + "Help function must have a return type `-> RenderResult`", + )), } } @@ -37,8 +49,8 @@ pub fn help_attr(item: TokenStream) -> TokenStream { Err(e) => return e.to_compile_error().into(), }; - // Validate return type - if let Err(e) = validate_return_type(&input_fn.sig) { + // Validate return type is RenderResult + if let Err(e) = validate_render_result_return(&input_fn.sig) { return e.to_compile_error().into(); } @@ -136,19 +148,17 @@ pub fn help_attr(item: TokenStream) -> TokenStream { impl ::mingling::HelpRequest for #struct_name { type Entry = #entry_type; - fn render_help(#prev_param: Self::Entry, __renderer_inner_result: &mut ::mingling::RenderResult) { + fn render_help(#prev_param: Self::Entry) -> ::mingling::RenderResult { #help_render_body } } ::mingling::macros::register_help!(#entry_type, #struct_name); - // Keep the original function for internal use with original params without __renderer_inner_result + // Keep the original function unchanged #(#fn_attrs)* - #vis fn #fn_name(#original_inputs) { - let mut dummy_r = ::mingling::RenderResult::default(); - let __renderer_inner_result = &mut dummy_r; + #vis fn #fn_name(#original_inputs) -> ::mingling::RenderResult { #fn_body } }; @@ -164,7 +174,7 @@ fn build_help_entry(struct_name: &Ident, entry_type: &TypePath) -> proc_macro2:: // SAFETY: The member_id check ensures that `any` contains a value of type `#entry_type`, // so downcasting to `#entry_type` is safe. let value = unsafe { any.downcast::<#entry_type>().unwrap_unchecked() }; - <#struct_name as ::mingling::HelpRequest>::render_help(value, __renderer_inner_result); + <#struct_name as ::mingling::HelpRequest>::render_help(value) } } } diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index 546416d..d0f603a 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -2,7 +2,7 @@ //! //! This crate is the **macro layer** of Mingling. Each `#[attribute]` or `!`-callable //! macro collects metadata into **compile-time global registries** (`OnceLock<Mutex<BTreeSet>>`). -//! At the end, [`gen_program!`] reads all registries and generates the final program struct +//! At the end, `gen_program!` reads all registries and generates the final program struct //! with all dispatchers, chains, renderers, and completions wired together. //! //! # How Macros Work Together @@ -47,16 +47,16 @@ //! //! | Macro | What it does | //! |-------|-------------| -//! | [`dispatcher!`] | Declares a command entry point and its argument type | -//! | [`macro@dispatcher_clap`] | Like `dispatcher!` but powered by `clap::Parser` | -//! | [`node!`] | Builds a [`Node`](https://docs.rs/mingling/latest/mingling/struct.Node.html) from a dot-separated path string | -//! | [`pack!`] | Creates a newtype wrapper around an inner type for use in Chain/Renderer | -//! | [`pack_structural!`] | Like `pack!` but also derives `StructuralData` for structured output | -//! | [`pack_err!`] | Creates an error struct with automatic `name` field | -//! | [`pack_err_structural!`] | Like `pack_err!` but also derives `StructuralData` for structured output | -//! | [`entry!`] | Creates a packed entry from string literals | +//! | `dispatcher!` | Declares a command entry point and its argument type | +//! | `dispatcher_clap!` | Like `dispatcher!` but powered by `clap::Parser` | +//! | `node!` | Builds a [`Node`](https://docs.rs/mingling/latest/mingling/struct.Node.html) from a dot-separated path string | +//! | `pack!` | Creates a newtype wrapper around an inner type for use in Chain/Renderer | +//! | `pack_structural!` | Like `pack!` but also derives `StructuralData` for structured output | +//! | `pack_err!` | Creates an error struct with automatic `name` field | +//! | `pack_err_structural!` | Like `pack_err!` but also derives `StructuralData` for structured output | +//! | `entry!` | Creates a packed entry from string literals | //! | [`#[derive(Groupped)]`](derive@Groupped) | Makes a type recognizable by the framework's type registry | -//! | [`#[derive(StructuralData)]`](derive@StructuralData) | Marks a type as eligible for structured output (JSON/YAML/etc.) | +//! | `#[derive(StructuralData)]` | Marks a type as eligible for structured output (JSON/YAML/etc.) | //! | [`#[derive(EnumTag)]`](derive@EnumTag) | Adds enum variant metadata (name, description) | //! //! ## Phase 2: Processing & Rendering Registration @@ -66,28 +66,27 @@ //! | [`#[chain]`](attr.chain.html) | Transforms a function into a chain processing step | //! | [`#[renderer]`](attr.renderer.html) | Transforms a function into a renderer for a type | //! | [`#[help]`](attr.help.html) | Defines help output for a command entry type | -//! | [`route!`] | Routes execution depending on a condition | -//! | [`empty_result!`] | Returns an empty result for early termination | -//! | [`r_print!`] / [`r_println!`] | Print to the `RenderResult` buffer inside a renderer | +//! | `route!` | Routes execution depending on a condition | +//! | `empty_result!` | Returns an empty result for early termination | //! | [`#[completion]`](attr.completion.html) | Registers a shell completion handler | //! //! ## Phase 3: Program Generation //! //! | Macro | What it does | //! |-------|-------------| -//! | [`gen_program!`] | **Final step**: reads all registries and generates the full program | -//! | [`suggest!`] | Generates suggestion logic for a dispatcher | -//! | [`suggest_enum!`] | Generates suggestion logic for an enum dispatcher | +//! | `gen_program!` | **Final step**: reads all registries and generates the full program | +//! | `suggest!` | Generates suggestion logic for a dispatcher | +//! | `suggest_enum!` | Generates suggestion logic for an enum dispatcher | //! //! ## Internal (used by the macros above) //! //! | Macro | What it does | //! |-------|-------------| -//! | [`register_type!`] | Registers a type in the packed-type registry | -//! | [`register_chain!`] | Registers a chain mapping in the chain registry | -//! | [`register_renderer!`] | Registers a renderer mapping in the renderer registry | -//! | [`register_dispatcher!`] | Registers a dispatcher for the `dispatch_tree` feature | -//! | [`register_help!`] | Registers a help request handler | +//! | `register_type!` | Registers a type in the packed-type registry | +//! | `register_chain!` | Registers a chain mapping in the chain registry | +//! | `register_renderer!` | Registers a renderer mapping in the renderer registry | +//! | `register_dispatcher!` | Registers a dispatcher for the `dispatch_tree` feature | +//! | `register_help!` | Registers a help request handler | //! | `program_fallback_gen!` | Generates fallback error types | //! | `program_final_gen!` | Generates the `ProgramCollect` impl and `ThisProgram` struct | //! | `program_comp_gen!` | Generates completion logic | @@ -99,12 +98,12 @@ //! //! | Feature | Macros enabled | //! |---------|---------------| -//! | `clap` | [`macro@dispatcher_clap`] | -//! | `comp` | [`#[completion]`](attr.completion.html), [`suggest!`], [`suggest_enum!`] | -//! | `extra_macros` | [`entry!`], [`empty_result!`], [`route!`], [`#[program_setup]`](attr.program_setup.html), [`macro@group`] | +//! | `clap` | `dispatcher_clap!` | +//! | `comp` | [`#[completion]`](attr.completion.html), `suggest!`, `suggest_enum!` | +//! | `extra_macros` | `entry!`, `empty_result!`, `route!`, [`#[program_setup]`](attr.program_setup.html), `group!` | //! | `dispatch_tree` | `register_dispatcher!` (enables trie-based command dispatch) | -//! | `structural_renderer` | [`#[derive(StructuralData)]`](derive@StructuralData), [`pack_structural!`], [`pack_err_structural!`], [`macro@group_structural`] | -//! | `structural_renderer` + `extra_macros` | [`group_structural!`], [`pack_err_structural!`] | +//! | `structural_renderer` | `#[derive(StructuralData)]`, `pack_structural!`, `pack_err_structural!`, `group_structural!` | +//! | `structural_renderer` + `extra_macros` | `group_structural!`, `pack_err_structural!` | //! | `async` | Enables async `#[chain]` functions | //! | `repl` | Enables REPL execution loop | //! @@ -115,7 +114,7 @@ //! entries contain the **token-stream representation** of match arms, type mappings, //! and struct definitions. //! -//! When [`gen_program!`] is called, it reads all registries, concatenates their +//! When `gen_program!` is called, it reads all registries, concatenates their //! entries, and emits the complete program: //! //! ```rust,ignore @@ -170,7 +169,6 @@ mod pack; mod pack_err; #[cfg(feature = "extra_macros")] mod program_setup; -mod render; mod renderer; mod res_injection; #[cfg(feature = "structural_renderer")] @@ -241,10 +239,27 @@ pub(crate) fn check_duplicate_variant( /// Checks if a stored entry string contains the given variant name. /// Handles both "StructName => Variant," and "Self::Variant => ..." formats. fn entry_has_variant(entry: &str, variant_name: &str) -> bool { - entry.contains(&format!("=> {variant_name},")) - || entry.contains(&format!("=> {variant_name} ")) - || entry.contains(&format!("=> {variant_name}")) - || entry.contains(&format!(":: {variant_name} =>")) + let variant_match = format!("=> {variant_name}"); + + // "StructName => Variant," — exact match with trailing comma + if entry.contains(&format!("{variant_match},")) { + return true; + } + // "StructName => Variant " — exact match with trailing space + if entry.contains(&format!("{variant_match} ")) { + return true; + } + // "StructName => Variant" (fallback) — must NOT be followed by identifier chars + if let Some(idx) = entry.find(&variant_match) { + let after = idx + variant_match.len(); + if after >= entry.len() + || !entry[after..].starts_with(|c: char| c.is_alphanumeric() || c == '_') + { + return true; + } + } + // "Self::Variant => ..." — match-arm existence check format + entry.contains(&format!(":: {variant_name} =>")) } /// Registers an outside-type as a member of a program group without modifying its definition. @@ -584,13 +599,13 @@ pub fn route(input: TokenStream) -> TokenStream { /// crate::ResultEmpty::new(()).to_chain() /// ``` /// -/// This works because [`ResultEmpty`] is automatically generated by [`gen_program!`] +/// This works because [`ResultEmpty`] is automatically generated by `gen_program!` /// and implements the necessary trait conversions into [`ChainProcess`]. /// /// # See also /// /// - [`ResultEmpty`] — The type that represents an empty result. -/// - [`route!`] — For early-return from `Result` expressions. +/// - `route!` — For early-return from `Result` expressions. /// /// [`ResultEmpty`]: https://docs.rs/mingling/latest/mingling/type.ResultEmpty.html /// [`ChainProcess`]: https://docs.rs/mingling/latest/mingling/enum.ChainProcess.html @@ -675,8 +690,8 @@ pub fn empty_result(_input: TokenStream) -> TokenStream { /// /// # See also /// -/// - [`macro@dispatcher_clap`] — For clap-powered argument parsing. -/// - [`node!`] — For building custom [`Node`] paths. +/// - `dispatcher_clap!` — For clap-powered argument parsing. +/// - `node!` — For building custom [`Node`] paths. /// - [`#[chain]`](attr.chain.html) — For processing the dispatched entry. /// /// [`ChainProcess`]: https://docs.rs/mingling/latest/mingling/enum.ChainProcess.html @@ -687,95 +702,6 @@ pub fn dispatcher(input: TokenStream) -> TokenStream { dispatcher::dispatcher(input) } -/// Prints formatted text to the current [`RenderResult`] buffer within a -/// `#[renderer]` function. -/// -/// Unlike `print!`, this macro writes to the in-memory `RenderResult` buffer -/// rather than directly to stdout. The buffered output is flushed automatically -/// when the renderer returns, allowing the framework to control output timing -/// and capture (e.g., for testing or structural rendering to JSON/YAML). -/// -/// This macro requires a mutable reference to a [`RenderResult`] named -/// `__renderer_inner_result` to be in scope, which is automatically provided -/// inside `#[renderer]` and `#[help]` functions. -/// -/// # Syntax -/// -/// Same as [`format!`] / [`print!`]: -/// -/// ```rust,ignore -/// r_print!("Hello, {}!", name); -/// r_print!("Value: {value}"); -/// ``` -/// -/// # Example -/// -/// ```rust,ignore -/// use mingling::macros::{renderer, r_print}; -/// -/// #[renderer] -/// fn show_greeting(prev: Greeting) { -/// r_print!("Hello, {}!", *prev); -/// } -/// ``` -/// -/// # Difference from `r_println!` -/// -/// `r_print!` does **not** append a newline. Use [`r_println!`] for newline-terminated output. -/// -/// # Panics -/// -/// Does not panic under normal circumstances. The underlying `RenderResult` -/// buffer operations are infallible. -/// -/// [`RenderResult`]: https://docs.rs/mingling/latest/mingling/struct.RenderResult.html -#[proc_macro] -pub fn r_print(input: TokenStream) -> TokenStream { - render::r_print(input) -} - -/// Prints formatted text followed by a newline to the current [`RenderResult`] buffer -/// within a `#[renderer]` or `#[help]` function. -/// -/// Unlike `println!`, this macro writes to the in-memory `RenderResult` buffer -/// rather than directly to stdout. The buffered output is flushed automatically -/// when the renderer returns. -/// -/// This macro requires a mutable reference to a [`RenderResult`] named -/// `__renderer_inner_result` to be in scope, which is automatically provided -/// inside `#[renderer]` and `#[help]` functions. -/// -/// # Syntax -/// -/// Same as [`println!`]: -/// -/// ```rust,ignore -/// r_println!("Hello, {}!", name); -/// r_println!("Value: {value}"); -/// r_println!(); // just a newline -/// ``` -/// -/// # Example -/// -/// ```rust,ignore -/// use mingling::macros::{renderer, r_println}; -/// -/// #[renderer] -/// fn show_greeting(prev: Greeting) { -/// r_println!("Hello, {}!", *prev); -/// } -/// ``` -/// -/// # See also -/// -/// - [`r_print!`] for output without a trailing newline. -/// -/// [`RenderResult`]: https://docs.rs/mingling/latest/mingling/struct.RenderResult.html -#[proc_macro] -pub fn r_println(input: TokenStream) -> TokenStream { - render::r_println(input) -} - /// Declares a chain processing step that transforms one type into another /// within a Mingling pipeline. /// @@ -876,7 +802,7 @@ pub fn r_println(input: TokenStream) -> TokenStream { /// # Sync Example with Resource Injection /// /// ```rust,ignore -/// use mingling::macros::{chain, pack, gen_program, r_println}; +/// use mingling::macros::{chain, pack, gen_program}; /// /// #[derive(Default, Clone)] /// struct UserName(String); @@ -886,7 +812,6 @@ pub fn r_println(input: TokenStream) -> TokenStream { /// /// #[chain] /// fn greet(prev: HelloEntry, user_name: &UserName, count: &mut u64) -> Next { -/// r_println!("User: {:?}", user_name); /// *count += 1; /// Greeting::new(format!("Hello, {}!", user_name.0)) /// } @@ -954,32 +879,32 @@ pub fn chain(attr: TokenStream, item: TokenStream) -> TokenStream { /// The `#[renderer]` attribute converts a function into a renderer by: /// 1. Generating a hidden struct implementing the `Renderer` trait. /// 2. Registering the renderer mapping in the global renderer registry. -/// 3. Keeping the original function for direct calls. When called directly, -/// a new `RenderResult` is created and the renderer function writes its -/// output directly to the current terminal output buffer. -/// -/// Inside a `#[renderer]` function, you can use `r_print!` and `r_println!` -/// to write output to the `RenderResult` buffer. +/// 3. Keeping the original function for direct calls. /// /// # Syntax /// /// ```rust,ignore /// #[renderer] -/// fn render_my_type(prev: MyType) { -/// r_println!("Output: {:?}", *prev); +/// fn render_my_type(prev: MyType) -> RenderResult { +/// let mut result = RenderResult::new(); +/// writeln!(result, "Output: {:?}", *prev); +/// result /// } /// ``` /// /// # Example /// /// ```rust,ignore -/// use mingling::macros::{renderer, r_println, pack, gen_program}; +/// use mingling::macros::{renderer, pack, gen_program}; +/// use std::io::Write; /// /// pack!(Greeting = String); /// /// #[renderer] -/// fn render_greeting(prev: Greeting) { -/// r_println!("Hello, {}!", *prev); +/// fn render_greeting(prev: Greeting) -> RenderResult { +/// let mut result = RenderResult::new(); +/// writeln!(result, "Hello, {}!", *prev); +/// result /// } /// ``` /// @@ -998,13 +923,17 @@ pub fn chain(attr: TokenStream, item: TokenStream) -> TokenStream { /// /// ```rust,ignore /// #[renderer] -/// fn fallback_dispatcher_not_found(prev: ErrorDispatcherNotFound) { -/// r_println!("Unknown command: {}", prev.join(", ")); +/// fn fallback_dispatcher_not_found(prev: ErrorDispatcherNotFound) -> RenderResult { +/// let mut result = RenderResult::new(); +/// writeln!(result, "Unknown command: {}", prev.join(", ")); +/// result /// } /// /// #[renderer] -/// fn fallback_renderer_not_found(prev: ErrorRendererNotFound) { -/// r_println!("No renderer for `{}`", *prev); +/// fn fallback_renderer_not_found(prev: ErrorRendererNotFound) -> RenderResult { +/// let mut result = RenderResult::new(); +/// writeln!(result, "No renderer for `{}`", *prev); +/// result /// } /// ``` #[proc_macro_attribute] @@ -1159,7 +1088,7 @@ pub fn dispatcher_clap(attr: TokenStream, item: TokenStream) -> TokenStream { /// Creates a packed entry value from a list of string literals. /// /// This is a convenience macro for constructing entry wrapper types (created -/// via [`pack!`] or [`dispatcher!`]) with test data, typically used in unit tests +/// via `pack!` or `dispatcher!`) with test data, typically used in unit tests /// or quick prototypes. /// /// # Syntax @@ -1190,8 +1119,8 @@ pub fn dispatcher_clap(attr: TokenStream, item: TokenStream) -> TokenStream { /// /// # See also /// -/// - [`pack!`] — For creating the wrapper types used with `entry!`. -/// - [`dispatcher!`] — Which implicitly creates entry types via `pack!`. +/// - `pack!` — For creating the wrapper types used with `entry!`. +/// - `dispatcher!` — Which implicitly creates entry types via `pack!`. #[cfg(feature = "extra_macros")] #[proc_macro] pub fn entry(input: TokenStream) -> TokenStream { @@ -1218,10 +1147,10 @@ pub fn register_help(input: TokenStream) -> TokenStream { /// Registers a dispatcher at compile time for the `dispatch_tree` feature. /// -/// This macro is called internally by [`dispatcher!`] when the `dispatch_tree` +/// This macro is called internally by `dispatcher!` when the `dispatch_tree` /// feature is enabled. Each call stores the node name into the global /// `COMPILE_TIME_DISPATCHERS` registry and generates a static variable for the -/// dispatcher instance. This data is later consumed by [`gen_program!`] to +/// dispatcher instance. This data is later consumed by `gen_program!` to /// generate a character-level **Trie** for efficient command dispatch. /// /// The trie dispatch works by grouping commands by their character prefix, @@ -1238,7 +1167,7 @@ pub fn register_help(input: TokenStream) -> TokenStream { /// /// # See also /// -/// - [`dispatcher!`] — The primary way to declare dispatchers (calls this internally). +/// - `dispatcher!` — The primary way to declare dispatchers (calls this internally). /// - `dispatch_tree_gen` module — The trie generation logic. #[proc_macro] pub fn register_dispatcher(input: TokenStream) -> TokenStream { @@ -1258,33 +1187,39 @@ pub fn register_dispatcher(input: TokenStream) -> TokenStream { /// The macro works by: /// 1. Generating a hidden struct implementing the `HelpRequest` trait. /// 2. Registering the help mapping in the global `HELP_REQUESTS` registry. -/// 3. Keeping the original function for direct calls (with a dummy `RenderResult`). +/// 3. Keeping the original function for direct calls. /// -/// Inside a `#[help]` function, you can use [`r_print!`] and [`r_println!`] -/// to write help text to the `RenderResult` buffer. +/// Inside a `#[help]` function, you must manually create a `RenderResult` +/// and return it. Use `writeln!` on the result to +/// write help text. /// /// # Syntax /// /// ```rust,ignore /// #[help] -/// fn help_my_entry(prev: MyEntry) { -/// r_println!("Usage: myapp myentry [options]"); -/// r_println!(" Does something useful."); +/// fn help_my_entry(prev: MyEntry) -> RenderResult { +/// let mut result = RenderResult::new(); +/// writeln!(result, "Usage: myapp myentry [options]"); +/// writeln!(result, " Does something useful."); +/// result /// } /// ``` /// /// # Example /// /// ```rust,ignore -/// use mingling::macros::{help, r_println, pack, gen_program}; -/// use mingling::{prelude::*, setup::BasicProgramSetup}; +/// use mingling::macros::{help, pack, gen_program}; +/// use mingling::{prelude::*, setup::BasicProgramSetup, RenderResult}; +/// use std::io::Write; /// /// pack!(MyEntry = Vec<String>); /// /// #[help] -/// fn help_my_entry(prev: MyEntry) { -/// r_println!("Usage: myapp greet [name]"); -/// r_println!("Greets the user."); +/// fn help_my_entry(prev: MyEntry) -> RenderResult { +/// let mut result = RenderResult::new(); +/// writeln!(result, "Usage: myapp greet [name]"); +/// writeln!(result, "Greets the user."); +/// result /// } /// /// fn main() { @@ -1299,13 +1234,13 @@ pub fn register_dispatcher(input: TokenStream) -> TokenStream { /// /// - The function must have exactly one parameter (the entry type to provide help for). /// - The parameter type must be a single-segment type path (e.g., `MyEntry`, not `other::MyEntry`). -/// - The function must return `()`. +/// - The function must return `RenderResult`. /// - The function cannot be async. /// /// # See also /// /// - [`BasicProgramSetup`] — The setup that enables `--help` and `-h` flag processing. -/// - [`r_print!`] / [`r_println!`] — For outputting help text. +/// - `RenderResult` — The return type for help functions. /// - [`#[chain]`](attr.chain.html) — For processing the dispatched entry after help. /// /// [`BasicProgramSetup`]: https://docs.rs/mingling/latest/mingling/setup/struct.BasicProgramSetup.html @@ -1391,7 +1326,7 @@ pub fn derive_enum_tag(input: TokenStream) -> TokenStream { enum_tag::derive_enum_tag(input) } -/// Derive macro for [`StructuralData`], marking a type as eligible for structured +/// Derive macro for `StructuralData`, marking a type as eligible for structured /// structured output (JSON / YAML / TOML / RON). /// /// The type must also implement `serde::Serialize` — the generated @@ -1479,7 +1414,8 @@ pub fn derive_groupped_serialize(input: TokenStream) -> TokenStream { /// # Example /// /// ```rust,ignore -/// use mingling::macros::{dispatcher, chain, renderer, gen_program}; +/// use mingling::macros::{dispatcher, chain, renderer, gen_program, RenderResult}; +/// use std::io::Write; /// /// dispatcher!("hello", HelloCommand => HelloEntry); /// @@ -1489,8 +1425,10 @@ pub fn derive_groupped_serialize(input: TokenStream) -> TokenStream { /// } /// /// #[renderer] -/// fn render(prev: /* ... */) { -/// r_println!("Done!"); +/// fn render(prev: /* ... */) -> RenderResult { +/// let mut result = RenderResult::new(); +/// writeln!(result, "Done!"); +/// result /// } /// /// // Collect everything: @@ -1595,9 +1533,11 @@ pub fn program_comp_gen(_input: TokenStream) -> TokenStream { #[allow(unused)] #[doc(hidden)] #[::mingling::macros::renderer] - pub fn __render_completion(prev: CompletionSuggest) { + pub fn __render_completion(prev: CompletionSuggest) -> ::mingling::RenderResult { + let result = ::mingling::RenderResult::default(); let (ctx, suggest) = prev.inner; ::mingling::CompletionHelper::render_suggest::<crate::ThisProgram>(ctx, suggest); + result } }; @@ -1766,9 +1706,9 @@ pub fn program_fallback_gen(_input: TokenStream) -> TokenStream { /// impl ProgramCollect for MyProgram { /// type Enum = MyProgram; /// type ResultEmpty = ResultEmpty; -/// fn render(any, r) { /* dispatches to all registered renderers */ } +/// fn render(any) -> RenderResult { /* dispatches to all registered renderers */ } /// fn do_chain(any) -> ChainProcess { /* dispatches to all registered chain steps */ } -/// fn render_help(any, r) { /* dispatches to all registered help handlers */ } +/// fn render_help(any) -> RenderResult { /* dispatches to all registered help handlers */ } /// fn has_renderer(any) -> bool { /* checks renderer registry */ } /// fn has_chain(any) -> bool { /* checks chain registry */ } /// // (with comp feature) fn do_comp(...) @@ -2003,12 +1943,15 @@ pub fn program_final_gen(_input: TokenStream) -> TokenStream { let comp = quote! {}; // Build render function arms from stored entries - let render_fn = if renderer_tokens.is_empty() { - quote! { - fn render(_any: ::mingling::AnyOutput<Self::Enum>, _renderer_inner_result: &mut ::mingling::RenderResult) {} - } - } else { - let render_arms: Vec<_> = renderer_tokens.iter().map(|entry| { + let render_fn = + if renderer_tokens.is_empty() { + quote! { + fn render(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult { + ::mingling::RenderResult::default() + } + } + } else { + let render_arms: Vec<_> = renderer_tokens.iter().map(|entry| { let (struct_ident, variant_ident) = parse_entry_pair(entry); let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map); let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map); @@ -2017,19 +1960,19 @@ pub fn program_final_gen(_input: TokenStream) -> TokenStream { // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`, // so downcasting to `#variant_ident` is safe. let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() }; - <#resolved_struct as ::mingling::Renderer>::render(value, __renderer_inner_result); + <#resolved_struct as ::mingling::Renderer>::render(value) } } }).collect(); - quote! { - fn render(any: ::mingling::AnyOutput<Self::Enum>, __renderer_inner_result: &mut ::mingling::RenderResult) { - match any.member_id { - #(#render_arms)* - _ => (), + quote! { + fn render(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult { + match any.member_id { + #(#render_arms)* + _ => ::mingling::RenderResult::default(), + } } } - } - }; + }; // Build do_chain function (async and sync versions) let chain_arms_async: Vec<_> = chain_tokens.iter().map(|entry| { @@ -2145,12 +2088,12 @@ pub fn program_final_gen(_input: TokenStream) -> TokenStream { } #render_fn #do_chain_fn - fn render_help(any: ::mingling::AnyOutput<Self::Enum>, __renderer_inner_result: &mut ::mingling::RenderResult) { + fn render_help(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult { #[allow(unused_imports)] #(#pathf_uses)* match any.member_id { #(#help_tokens)* - _ => (), + _ => ::mingling::RenderResult::default(), } } fn has_renderer(any: &::mingling::AnyOutput<Self::Enum>) -> bool { diff --git a/mingling_macros/src/render.rs b/mingling_macros/src/render.rs deleted file mode 100644 index 10fce7d..0000000 --- a/mingling_macros/src/render.rs +++ /dev/null @@ -1,56 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::parse::Parser; -use syn::{Expr, Token}; - -pub fn r_print(input: TokenStream) -> TokenStream { - // Parse the input as format arguments - let parser = syn::punctuated::Punctuated::<Expr, Token![,]>::parse_terminated; - let format_args = match parser.parse(input) { - Ok(args) => args, - Err(e) => return e.to_compile_error().into(), - }; - - // Build the format macro call - let format_call = if format_args.is_empty() { - quote! { ::std::format!("") } - } else { - let args_iter = format_args.iter(); - quote! { ::std::format!(#(#args_iter),*) } - }; - - let expanded = quote! { - { - let formatted = #format_call; - ::mingling::RenderResult::print(__renderer_inner_result, &formatted) - } - }; - - expanded.into() -} - -pub fn r_println(input: TokenStream) -> TokenStream { - // Parse the input as format arguments - let parser = syn::punctuated::Punctuated::<Expr, Token![,]>::parse_terminated; - let format_args = match parser.parse(input) { - Ok(args) => args, - Err(e) => return e.to_compile_error().into(), - }; - - // Build the format macro call - let format_call = if format_args.is_empty() { - quote! { ::std::format!("") } - } else { - let args_iter = format_args.iter(); - quote! { ::std::format!(#(#args_iter),*) } - }; - - let expanded = quote! { - { - let formatted = #format_call; - ::mingling::RenderResult::println(__renderer_inner_result, &formatted) - } - }; - - expanded.into() -} diff --git a/mingling_macros/src/renderer.rs b/mingling_macros/src/renderer.rs index 880c50f..f47511c 100644 --- a/mingling_macros/src/renderer.rs +++ b/mingling_macros/src/renderer.rs @@ -6,18 +6,33 @@ use syn::{ItemFn, ReturnType, Signature, Type, TypePath, parse_macro_input}; use crate::get_global_set; use crate::res_injection::{extract_args_info, generate_immut_resource_bindings}; -/// Extracts and returns the return type from the function signature (or None for `()` / no return type). -fn extract_return_type(sig: &Signature) -> Option<syn::Type> { +/// Validates that the function returns `::mingling::RenderResult`. +fn validate_render_result_return(sig: &Signature) -> syn::Result<()> { match &sig.output { ReturnType::Type(_, ty) => { + // Check if the return type is RenderResult match &**ty { - // `()` means no custom return type - Type::Tuple(tuple) if tuple.elems.is_empty() => None, - // Any other return type is allowed - custom_ty => Some((*custom_ty).clone()), + Type::Path(type_path) => { + let segments = &type_path.path.segments; + let last_seg = segments.last().map(|s| s.ident.to_string()); + match last_seg.as_deref() { + Some("RenderResult") => Ok(()), + _ => Err(syn::Error::new( + ty.span(), + "Renderer function must return `RenderResult`", + )), + } + } + _ => Err(syn::Error::new( + ty.span(), + "Renderer function must return `RenderResult`", + )), } } - ReturnType::Default => None, + ReturnType::Default => Err(syn::Error::new( + sig.span(), + "Renderer function must have a return type `-> RenderResult`", + )), } } @@ -44,8 +59,10 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream { Err(e) => return e.to_compile_error().into(), }; - // Validate return type – now returns Some(type) if custom type, None if () - let return_type = extract_return_type(&input_fn.sig); + // Validate that the function returns RenderResult + if let Err(e) = validate_render_result_return(&input_fn.sig) { + return e.to_compile_error().into(); + } // Get function body statements let fn_body_stmts: Vec<syn::Stmt> = input_fn.block.stmts.clone(); @@ -76,23 +93,6 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream { let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), program_type); let mut_resources: Vec<_> = resources.iter().filter(|r| r.is_mut).collect(); - // Determine public return type and the expression to return dummy_r - let (public_return_type, result_return) = if let Some(custom_ty) = &return_type { - // User specified a custom return type (e.g. -> String) - let ret_ty = quote! { #custom_ty }; - let expr = quote! { dummy_r.into() }; - (ret_ty, expr) - } else { - // Return type is () — no custom return type specified - let ret_ty = quote! { () }; - let expr = quote! { - if !dummy_r.is_empty() { - ::std::println!("{}", &*dummy_r); - } - }; - (ret_ty, expr) - }; - let inner_body_with_resources = if has_mut_resources { let mut wrapped = quote! { #(#fn_body_stmts)* }; for res in mut_resources.iter().rev() { @@ -110,8 +110,7 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream { }; // Build the Renderer::render body with resource injection - // Renderer::render returns (), output goes through __renderer_inner_result parameter. - // Resources are injected from the program context here. + // The user's body now directly creates and returns a RenderResult. let render_fn_body = if has_resources { quote! { #(#immut_resource_stmts)* @@ -121,22 +120,13 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream { quote! { #inner_body_with_resources } }; - // Build the original function body - // The original function preserves the user's signature and return type. + // The original function preserves the user's exact signature and body. // Resource parameters are passed directly by the caller, NOT injected from context. - let original_fn_body = { - quote! { - let mut dummy_r = ::mingling::RenderResult::default(); - { - let __renderer_inner_result = &mut dummy_r; - #(#fn_body_stmts)* - } - #result_return - } - }; - - // Keep the original function signature unchanged (same params as user wrote) let original_inputs = input_fn.sig.inputs.clone(); + let original_return_type = match &input_fn.sig.output { + ReturnType::Type(_, ty) => quote! { #ty }, + ReturnType::Default => unreachable!("Already validated that return type is RenderResult"), + }; let expanded = quote! { #(#fn_attrs)* @@ -149,15 +139,15 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream { impl ::mingling::Renderer for #struct_name { type Previous = #previous_type; - fn render(#prev_param: Self::Previous, __renderer_inner_result: &mut ::mingling::RenderResult) { + fn render(#prev_param: Self::Previous) -> ::mingling::RenderResult { #render_fn_body } } - // Keep the original function for internal use (without r parameter) + // Keep the original function unchanged #(#fn_attrs)* - #vis fn #fn_name(#original_inputs) -> #public_return_type { - #original_fn_body + #vis fn #fn_name(#original_inputs) -> #original_return_type { + #(#fn_body_stmts)* } }; diff --git a/mingling_macros/src/res_injection.rs b/mingling_macros/src/res_injection.rs index f2952cc..09da889 100644 --- a/mingling_macros/src/res_injection.rs +++ b/mingling_macros/src/res_injection.rs @@ -160,10 +160,7 @@ pub(crate) fn generate_immut_resource_bindings<'a>( /// Generates a unique binding name for a mutable resource variable. fn mut_res_binding_name(var_name: &Ident) -> Ident { - syn::Ident::new( - &format!("__{}_binding", var_name), - var_name.span(), - ) + syn::Ident::new(&format!("__{}_binding", var_name), var_name.span()) } /// Wraps the function body in nested `__modify_res_and_return_route` closures for diff --git a/mingling_macros/src/structural_data.rs b/mingling_macros/src/structural_data.rs index 37fee0f..5350d7e 100644 --- a/mingling_macros/src/structural_data.rs +++ b/mingling_macros/src/structural_data.rs @@ -222,7 +222,6 @@ impl syn::parse::Parse for PackStructuralInput { /// impl ::mingling::StructuralData for Info {} /// ``` pub(crate) fn group_structural(input: TokenStream) -> TokenStream { - // Parse the same input as group! let input_parsed = syn::parse_macro_input!(input as GroupStructuralInput); @@ -269,11 +268,18 @@ pub(crate) fn group_structural(input: TokenStream) -> TokenStream { proc_macro2::Span::call_site(), ); - // Generate the appropriate `use` statement + // Generate the appropriate `use` statement for the original type + // (consistent with gen_type_use in group_impl.rs) let type_use = if type_path.path.segments.len() > 1 { quote! { #[allow(unused_imports)] use #type_path; } } else { - let ident = &type_name; + let ident = type_path + .path + .segments + .last() + .expect("TypePath must have at least one segment") + .ident + .clone(); quote! { #[allow(unused_imports)] use super::#ident; } }; diff --git a/mingling_pathf/Cargo.toml b/mingling_pathf/Cargo.toml index 84bb260..0d4e37a 100644 --- a/mingling_pathf/Cargo.toml +++ b/mingling_pathf/Cargo.toml @@ -2,8 +2,11 @@ name = "mingling_pathf" version.workspace = true edition.workspace = true +authors = ["Weicao-CatilGrass"] license.workspace = true repository.workspace = true +readme = "README.md" +description = "A library for automatically finding internal types generated by Mingling" [dependencies] syn.workspace = true diff --git a/mingling_pathf/README.md b/mingling_pathf/README.md index 6d8857a..89d1811 100644 --- a/mingling_pathf/README.md +++ b/mingling_pathf/README.md @@ -18,16 +18,16 @@ Enable the `pathf` feature in `Cargo.toml`: ```toml [dependencies.mingling] # Used to modify the generation behavior of `gen_program!` -features = ["pathf"] +features = ["pathf"] [build-dependencies.mingling] # Provides the `analyze_and_build_type_mapping` function -features = ["builds", "pathf"] +features = ["builds", "pathf"] ``` Create a `build.rs` in the project root: -```rust +```rust,ignore fn main() { mingling::build::analyze_and_build_type_mapping(); } diff --git a/mingling_pathf/src/config.rs b/mingling_pathf/src/config.rs index 6758264..1199b2c 100644 --- a/mingling_pathf/src/config.rs +++ b/mingling_pathf/src/config.rs @@ -1,3 +1,9 @@ +//! Configuration for the module pathfinder analysis. +//! +//! This module defines [`PathfinderConfig`], which controls behavior such as +//! whether dispatch-tree related types (`__internal_dispatcher_*`) should be +//! extracted. + /// Configuration for the module pathfinder analysis. /// /// Controls behavior such as whether dispatch-tree related types diff --git a/mingling_pathf/src/error.rs b/mingling_pathf/src/error.rs index 025ceed..70a8f6e 100644 --- a/mingling_pathf/src/error.rs +++ b/mingling_pathf/src/error.rs @@ -1,3 +1,9 @@ +//! Errors that can occur during the pathfinding process for Rust module resolution. +//! +//! This module defines all possible failure modes when traversing the module graph +//! of a Rust project, including I/O failures, missing modules, invalid path +//! attributes, missing entry points, and syntax parsing errors. + use std::fmt; use std::path::PathBuf; @@ -24,10 +30,7 @@ pub enum MinglingPathfinderError { /// /// `file` is the file containing the invalid attribute. /// `path_attr` is the value of the `#[path]` attribute. - PathPointsOutside { - file: PathBuf, - path_attr: String, - }, + PathPointsOutside { file: PathBuf, path_attr: String }, /// No entry point file (`main.rs`, `lib.rs`, or any file under `bin/`) was found. NoEntryPointFound, @@ -36,23 +39,33 @@ pub enum MinglingPathfinderError { /// /// `path` is the file that failed to parse. /// `message` contains details from the parser. - SynError { - path: PathBuf, - message: String, - }, + SynError { path: PathBuf, message: String }, } impl fmt::Display for MinglingPathfinderError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::IoError(e) => write!(f, "IO error: {e}"), - Self::ModuleNotFound { parent, module_name } => { - write!(f, "Module `{module_name}` not found relative to {}", parent.display()) + Self::ModuleNotFound { + parent, + module_name, + } => { + write!( + f, + "Module `{module_name}` not found relative to {}", + parent.display() + ) } Self::PathPointsOutside { file, path_attr } => { - write!(f, "#[path = \"{path_attr}\"] in {} points outside the project", file.display()) + write!( + f, + "#[path = \"{path_attr}\"] in {} points outside the project", + file.display() + ) + } + Self::NoEntryPointFound => { + write!(f, "No entry point found (main.rs, lib.rs, or bin/*.rs)") } - Self::NoEntryPointFound => write!(f, "No entry point found (main.rs, lib.rs, or bin/*.rs)"), Self::SynError { path, message } => { write!(f, "Failed to parse {}: {message}", path.display()) } diff --git a/mingling_pathf/src/lib.rs b/mingling_pathf/src/lib.rs index 81b2df6..557ae45 100644 --- a/mingling_pathf/src/lib.rs +++ b/mingling_pathf/src/lib.rs @@ -1,3 +1,6 @@ +#![allow(clippy::needless_doctest_main)] +#![doc = include_str!("../README.md")] + pub mod config; pub mod error; pub mod module_pathf; diff --git a/mingling_pathf/src/module_pathf.rs b/mingling_pathf/src/module_pathf.rs index d06be9b..f0a06d1 100644 --- a/mingling_pathf/src/module_pathf.rs +++ b/mingling_pathf/src/module_pathf.rs @@ -1,3 +1,9 @@ +//! A module for mapping Rust module paths to source files. +//! +//! This module provides functionality to analyze the module structure of a Rust crate +//! and determine the effective module path for each source file, taking into account +//! `pub use` re-exports that can cause modules to be hoisted to parent paths. + use std::collections::HashMap; use std::path::{Path, PathBuf}; use syn::{Item, UseTree}; @@ -10,7 +16,6 @@ use crate::error::MinglingPathfinderError; /// effective module path (e.g., `crate::foo::bar`). #[derive(Debug, Clone)] pub struct MappingItem { - /// The path of the source file (relative to the crate root, with `./` prefix). file_path: PathBuf, @@ -349,11 +354,7 @@ fn propagate_children(parent_file: &Path, ctx: &mut Context) { .cloned() .unwrap_or_else(|| "crate".to_string()); - let reexported = ctx - .reexports - .get(parent_file) - .cloned() - .unwrap_or_default(); + let reexported = ctx.reexports.get(parent_file).cloned().unwrap_or_default(); let Some(children) = ctx.children.get(parent_file).cloned() else { return; @@ -367,8 +368,7 @@ fn propagate_children(parent_file: &Path, ctx: &mut Context) { format!("{}::{}", parent_effective, child.name) }; - ctx.effective_paths - .insert(child.file.clone(), effective); + ctx.effective_paths.insert(child.file.clone(), effective); propagate_children(&child.file, ctx); } } diff --git a/mingling_pathf/src/pattern_analyzer.rs b/mingling_pathf/src/pattern_analyzer.rs index c4b1971..3765971 100644 --- a/mingling_pathf/src/pattern_analyzer.rs +++ b/mingling_pathf/src/pattern_analyzer.rs @@ -1,3 +1,17 @@ +//! This module defines the core pattern analysis system used to parse and extract +//! importable/referenceable items (like structs, enums, functions, etc.) from Rust source files. +//! +//! It provides a pluggable architecture via the `AnalyzePattern` trait, allowing different +//! syntactic patterns to be registered and applied. Built-in patterns cover common structures +//! such as basic structs, packs, groups, derives, chains, renderers, help, completion, and +//! dispatch patterns (both standard and clap-based). +//! +//! The entry points are: +//! - [`init()`] — creates a default `PatternAnalyzer` with all built-in patterns. +//! - [`init_with_config()`] — creates a `PatternAnalyzer` with a given `PathfinderConfig`. +//! - [`PatternAnalyzer::analyze_file()`] / [`PatternAnalyzer::analyze_file_items()`] — run +//! analysis on a single file. + use std::collections::HashSet; use std::path::Path; diff --git a/mingling_pathf/src/patterns.rs b/mingling_pathf/src/patterns.rs index b3e0cd3..9801e9b 100644 --- a/mingling_pathf/src/patterns.rs +++ b/mingling_pathf/src/patterns.rs @@ -1,10 +1,12 @@ +//! Mingling path matching patterns for command routing and field mapping. + pub use basic_struct::*; pub use chain::*; pub use completion::*; pub use dispatcher::*; pub use dispatcher_clap::*; -pub use groupped_derive::*; pub use group::*; +pub use groupped_derive::*; pub use help::*; pub use pack::*; pub use renderer::*; @@ -14,8 +16,8 @@ mod chain; mod completion; mod dispatcher; mod dispatcher_clap; -mod groupped_derive; mod group; +mod groupped_derive; mod help; mod pack; mod renderer; diff --git a/mingling_pathf/src/patterns/basic_struct.rs b/mingling_pathf/src/patterns/basic_struct.rs index eeb665a..09e8e70 100644 --- a/mingling_pathf/src/patterns/basic_struct.rs +++ b/mingling_pathf/src/patterns/basic_struct.rs @@ -1,3 +1,7 @@ +//! The `BasicStructPattern` matches `struct` definitions in Rust source code. +//! It identifies root-level structs and structs nested inside inline modules, +//! returning their names and optional module path for analysis. + use syn::Item; use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; diff --git a/mingling_pathf/src/patterns/chain.rs b/mingling_pathf/src/patterns/chain.rs index 10d698e..6393440 100644 --- a/mingling_pathf/src/patterns/chain.rs +++ b/mingling_pathf/src/patterns/chain.rs @@ -1,3 +1,7 @@ +//! The `ChainPattern` matches functions annotated with `#[chain]` and +//! extracts the generated internal struct name (e.g., `__internal_chain_<fn_name>`). +//! This is used to track chained handler functions for code generation or analysis. + use syn::Item; use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; @@ -63,10 +67,7 @@ fn collect_from_item(item: &Item, current_mod: &str, items: &mut Vec<AnalyzeItem } fn has_attr(attrs: &[syn::Attribute], name: &str) -> bool { - attrs.iter().any(|a| { - a.path() - .segments - .last() - .is_some_and(|s| s.ident == name) - }) + attrs + .iter() + .any(|a| a.path().segments.last().is_some_and(|s| s.ident == name)) } diff --git a/mingling_pathf/src/patterns/completion.rs b/mingling_pathf/src/patterns/completion.rs index 7e4cd09..5427b93 100644 --- a/mingling_pathf/src/patterns/completion.rs +++ b/mingling_pathf/src/patterns/completion.rs @@ -1,3 +1,7 @@ +//! The `CompletionPattern` matches functions annotated with `#[completion(T)]` and +//! extracts the generated internal struct name (e.g., `__internal_completion_<fn_name>`). +//! This is used to track completion handler functions for code generation or analysis. + use syn::Item; use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; @@ -56,10 +60,7 @@ fn collect_from_item(item: &Item, current_mod: &str, items: &mut Vec<AnalyzeItem } fn has_attr(attrs: &[syn::Attribute], name: &str) -> bool { - attrs.iter().any(|a| { - a.path() - .segments - .last() - .is_some_and(|s| s.ident == name) - }) + attrs + .iter() + .any(|a| a.path().segments.last().is_some_and(|s| s.ident == name)) } diff --git a/mingling_pathf/src/patterns/dispatcher.rs b/mingling_pathf/src/patterns/dispatcher.rs index b9f147d..6796a2c 100644 --- a/mingling_pathf/src/patterns/dispatcher.rs +++ b/mingling_pathf/src/patterns/dispatcher.rs @@ -1,3 +1,16 @@ +//! The `DispatcherPattern` matches invocations of the `dispatcher!` macro and +//! extracts the generated type names from its arguments. It supports: +//! - `Entry*` — the entry type (always generated) +//! - `CMD*` — the dispatcher struct (always generated) +//! - `__internal_dispatcher_*` — the dispatch tree static (when `use_dispatch_tree` is `true`) +//! +//! Supported forms: +//! - Explicit: `dispatcher!("greet", CMDGreet => EntryGreet)` +//! - Implicit: `dispatcher!("greet")` — infers `CMDGreet` and `EntryGreet` +//! - With braces: `dispatcher! { ... }` +//! +//! This pattern is used to track dispatcher types for code generation or analysis. + use syn::Item; use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; diff --git a/mingling_pathf/src/patterns/dispatcher_clap.rs b/mingling_pathf/src/patterns/dispatcher_clap.rs index 2e1ec6c..1a86ad5 100644 --- a/mingling_pathf/src/patterns/dispatcher_clap.rs +++ b/mingling_pathf/src/patterns/dispatcher_clap.rs @@ -1,3 +1,17 @@ +//! The `DispatcherClapPattern` matches structs annotated with `#[dispatcher_clap(...)]` and +//! extracts key items for code generation or analysis: +//! - The entry struct name (always) +//! - The dispatcher command struct (`CMD*`, always) +//! - The error type, if `error = ErrorType` is specified +//! - The help internal struct, if `help = true` is specified +//! - The `__internal_dispatcher_*` dispatch tree static, if `use_dispatch_tree` is enabled +//! +//! Supported forms: +//! - `#[dispatcher_clap("greet", CMDGreet)] struct EntryGreet { ... }` +//! - `#[dispatcher_clap("greet", CMDGreet, error = ErrorGreet)] struct EntryGreet { ... }` +//! - `#[dispatcher_clap("greet", CMDGreet, help = true)] struct EntryGreet { ... }` +//! - `#[dispatcher_clap("greet", CMDGreet, error = ErrorGreet, help = true)] struct EntryGreet { ... }` + use syn::Item; use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; diff --git a/mingling_pathf/src/patterns/group.rs b/mingling_pathf/src/patterns/group.rs index 99d1137..0e4b50d 100644 --- a/mingling_pathf/src/patterns/group.rs +++ b/mingling_pathf/src/patterns/group.rs @@ -1,3 +1,7 @@ +//! The `GroupPattern` matches the `group!` and `group_structural!` macros and +//! extracts the type name or alias defined within them. +//! This is used to track type groups for code generation or analysis. + use syn::Item; use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; diff --git a/mingling_pathf/src/patterns/groupped_derive.rs b/mingling_pathf/src/patterns/groupped_derive.rs index 8491121..91daaef 100644 --- a/mingling_pathf/src/patterns/groupped_derive.rs +++ b/mingling_pathf/src/patterns/groupped_derive.rs @@ -1,3 +1,8 @@ +//! The `GrouppedDerivePattern` matches structs, enums, and unions annotated with +//! `#[derive(Groupped)]` or `#[derive(GrouppedSerialize)]` (or any combination +//! with other derives). It also recurses into `mod` items to find nested types. +//! This is used to track grouped items for code generation or analysis. + use syn::Item; use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; diff --git a/mingling_pathf/src/patterns/help.rs b/mingling_pathf/src/patterns/help.rs index 357626b..628f4ac 100644 --- a/mingling_pathf/src/patterns/help.rs +++ b/mingling_pathf/src/patterns/help.rs @@ -1,3 +1,7 @@ +//! The `HelpPattern` matches functions annotated with `#[help]` and +//! extracts the generated internal struct name (e.g., `__internal_help_<fn_name>`). +//! This is used to track help functions for code generation or analysis. + use syn::Item; use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; @@ -56,10 +60,7 @@ fn collect_from_item(item: &Item, current_mod: &str, items: &mut Vec<AnalyzeItem } fn has_attr(attrs: &[syn::Attribute], name: &str) -> bool { - attrs.iter().any(|a| { - a.path() - .segments - .last() - .is_some_and(|s| s.ident == name) - }) + attrs + .iter() + .any(|a| a.path().segments.last().is_some_and(|s| s.ident == name)) } diff --git a/mingling_pathf/src/patterns/pack.rs b/mingling_pathf/src/patterns/pack.rs index f025f7d..c80fb65 100644 --- a/mingling_pathf/src/patterns/pack.rs +++ b/mingling_pathf/src/patterns/pack.rs @@ -1,3 +1,7 @@ +//! The `PackPattern` matches types defined by `pack!`, `pack_err!`, `pack_structural!`, and `pack_err_structural!` macros. +//! It extracts the registered type name (e.g., `TypeName` from `pack!(TypeName = InnerType)`). +//! This is used to track packed type definitions for code generation or analysis. + use syn::Item; use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; @@ -40,12 +44,13 @@ impl AnalyzePattern for PackPattern { if let Some((_, nested)) = &item_mod.content { for n in nested { if let Item::Macro(m) = n - && let Some(name) = try_extract_pack_name(m) { - items.push(AnalyzeItem { - module: item_mod.ident.to_string(), - item_name: name, - }); - } + && let Some(name) = try_extract_pack_name(m) + { + items.push(AnalyzeItem { + module: item_mod.ident.to_string(), + item_name: name, + }); + } } } } @@ -89,10 +94,11 @@ fn try_extract_pack_name(m: &syn::ItemMacro) -> Option<String> { // Check if `=` follows if let Some(proc_macro2::TokenTree::Punct(p)) = iter.next() - && p.as_char() == '=' { - // pack!(TypeName = InnerType) - return Some(type_name); - } + && p.as_char() == '=' + { + // pack!(TypeName = InnerType) + return Some(type_name); + } // pack_err!(TypeName) — only a single ident return Some(type_name); diff --git a/mingling_pathf/src/patterns/renderer.rs b/mingling_pathf/src/patterns/renderer.rs index 410ae14..c2e9ca9 100644 --- a/mingling_pathf/src/patterns/renderer.rs +++ b/mingling_pathf/src/patterns/renderer.rs @@ -1,3 +1,7 @@ +//! The `RendererPattern` matches functions annotated with `#[renderer]` and +//! extracts the generated internal struct name (e.g., `__internal_renderer_<fn_name>`). +//! This is used to track rendering functions for code generation or analysis. + use syn::Item; use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; @@ -56,10 +60,7 @@ fn collect_from_item(item: &Item, current_mod: &str, items: &mut Vec<AnalyzeItem } fn has_attr(attrs: &[syn::Attribute], name: &str) -> bool { - attrs.iter().any(|a| { - a.path() - .segments - .last() - .is_some_and(|s| s.ident == name) - }) + attrs + .iter() + .any(|a| a.path().segments.last().is_some_and(|s| s.ident == name)) } diff --git a/mingling_pathf/src/type_mapping_builder.rs b/mingling_pathf/src/type_mapping_builder.rs index 3422af8..0965b47 100644 --- a/mingling_pathf/src/type_mapping_builder.rs +++ b/mingling_pathf/src/type_mapping_builder.rs @@ -1,3 +1,7 @@ +//! This module contains the matching patterns for `mingling_pathf`. +//! It provides the core logic for analyzing crate types and generating +//! type mapping files used by the pathfinder system. + use std::collections::HashSet; use std::path::Path; diff --git a/mingling_pathf/test/Cargo.lock b/mingling_pathf/test/Cargo.lock index e5fd23a..1260662 100644 --- a/mingling_pathf/test/Cargo.lock +++ b/mingling_pathf/test/Cargo.lock @@ -9,10 +9,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" [[package]] -name = "mingling_pathf" +name = "just_fmt" version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" + +[[package]] +name = "mingling_pathf" +version = "0.3.0" dependencies = [ - "just_fmt", + "just_fmt 0.2.0", "proc-macro2", "syn", ] @@ -50,7 +56,7 @@ dependencies = [ name = "test-mingling-pathf" version = "0.1.0" dependencies = [ - "just_fmt", + "just_fmt 0.1.2", "mingling_pathf", ] diff --git a/mingling_picker/Cargo.toml b/mingling_picker/Cargo.toml new file mode 100644 index 0000000..ddab33c --- /dev/null +++ b/mingling_picker/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "mingling_picker" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +authors = ["Weicao-CatilGrass"] +readme = "README.md" +description = "Mingling's lightweight argument parser" + +[features] +mingling_support = ["dep:mingling_core", "mingling_picker_macros/mingling_support"] + +[dependencies] +mingling_core = { workspace = true, optional = true } +mingling_picker_macros.workspace = true +just_fmt.workspace = true diff --git a/mingling_picker/README.md b/mingling_picker/README.md new file mode 100644 index 0000000..db67825 --- /dev/null +++ b/mingling_picker/README.md @@ -0,0 +1,47 @@ +# Mingling Picker + +A command-line argument parser for [Mingling](https://github.com/mingling-rs/mingling), enabled by the `mingling/picker` feature. + +```toml +[dependencies.mingling] +version = "0.3.0" +features = [ + "picker" +] +``` + +Of course, you can also use it as a standalone crate by replacing `mingling::picker` with `mingling_picker`: + +```toml +[dependencies] +mingling_picker = "0.3.0" +``` + +## Chained Argument Parser + +Provides a clean chained-call API for declaring arguments to parse: + +```rust +use mingling_picker::prelude::*; + +let args: Vec<&str> = vec!["--name", "Bob", "--age", "24"]; + +let (name, age) = args + .pick(&arg![name: String]) + .or(|| "Alice".to_string()) + .pick(&arg![age: i32]) + .or(|| 24) + .post(|num| num.clamp(0, 120)) + .unwrap(); + +assert_eq!(name, "Bob".to_string()); +assert_eq!(age, 24); +``` + +## Parsing Function Library + +Provides a pure function library `parselib` for analyzing the structure of command-line arguments. + +```rust +use mingling_picker::parselib::*; +``` diff --git a/mingling_picker/src/arg.rs b/mingling_picker/src/arg.rs new file mode 100644 index 0000000..78ad539 --- /dev/null +++ b/mingling_picker/src/arg.rs @@ -0,0 +1,299 @@ +use crate::Pickable; +use std::marker::PhantomData; + +/// Represents a constraint definition for a parameter selection. +/// +/// This structure describes the constraints that a command-line parameter (Picker parameter item) +/// should satisfy, including its full name list (with aliases), short name form, and whether it is +/// positional. +/// +/// # Field Descriptions +/// +/// - `full`: Full name or alias list. For example, `["config", "cfg"]` means the parameter can be +/// matched with either `--config` or `--cfg`. Must contain at least one non-empty string. +/// +/// - `short`: Short name (single character). For example, `Some('c')` means it can be passed using +/// the `-c` form. If set to `None`, the short name form is not supported. +/// +/// - `positional`: Whether the parameter is positional (i.e., an argument without a flag). +/// - `true`: The parameter is positional; it is matched by its position in the command line rather +/// than by a `--name` or `-n` flag. +/// - `false`: The parameter is a named (flag-based) parameter. +/// +/// - `_type`: PhantomData to hold the type parameter. +#[derive(Default, Clone, Copy)] +pub struct PickerArg<'a, Type> +where + Type: Pickable<'a>, +{ + /// Full name, may include variant names (aliases), e.g., `["config", "cfg"]`. + pub full: &'a [&'a str], + + /// Short name, e.g., `'c'`. + pub short: Option<char>, + + /// Whether the parameter is positional (no flag, matched by position). + pub positional: bool, + + /// PhantomData to hold the type parameter. + pub internal_type: PhantomData<Type>, +} + +impl<'a, Type> From<&'a PickerArg<'a, Type>> for PickerArg<'a, Type> +where + Type: Pickable<'a>, +{ + fn from(value: &'a PickerArg<'a, Type>) -> Self { + PickerArg { + full: value.full, + short: value.short, + positional: value.positional, + internal_type: PhantomData, + } + } +} + +impl<'a, Type> PickerArg<'a, Type> +where + Type: Pickable<'a>, +{ + /// Creates a new `PickerArg` with the provided parameters. + pub fn new(full: &'a [&'a str], short: Option<char>, positional: bool) -> Self { + Self { + full, + short, + positional, + internal_type: PhantomData, + } + } + + /// Returns the full name list (including aliases). + pub fn full(&self) -> &'a [&'a str] { + self.full + } + + /// Returns the short name, if any. + pub fn short(&self) -> Option<char> { + self.short + } + + /// Returns whether the parameter is positional. + /// + /// If `full` is empty or `short` is `None`, the parameter is considered positional + /// regardless of the stored value. + pub fn is_positional(&self) -> bool { + if self.full.is_empty() && self.short.is_none() { + true + } else { + self.positional + } + } + + /// Sets the full name list. + pub fn set_full(&mut self, full: &'a [&'a str]) { + self.full = full; + } + + /// Sets the short name. + pub fn set_short(&mut self, short: Option<char>) { + self.short = short; + } + + /// Sets whether the parameter is positional. + pub fn set_positional(&mut self, positional: bool) { + self.positional = positional; + } + + /// Sets the full name list and returns self. + pub fn with_full(mut self, full: &'a [&'a str]) -> Self { + self.full = full; + self + } + + /// Clears the full name list (sets it to an empty slice) and returns self. + pub fn without_full(mut self) -> Self { + self.full = &[]; + self + } + + /// Sets the short name to the given character and returns self. + pub fn with_short(mut self, short: char) -> Self { + self.short = Some(short); + self + } + + /// Clears the short name (sets it to None) and returns self. + pub fn without_short(mut self) -> Self { + self.short = None; + self + } + + /// Sets whether the parameter is positional and returns self. + pub fn with_positional(mut self, positional: bool) -> Self { + self.positional = positional; + self + } +} + +/// Describes the attribute (behavior) of a command-line parameter. +/// +/// The ordering reflects parse priority (higher = parsed first): +/// `PositionalMulti < Positional < Flag < Single < Multi` +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum PickerArgAttr { + /// Positional argument that accepts multiple values (e.g., multiple input files). + PositionalMulti, + + /// Positional argument matched by its position (e.g., an input file). + #[default] + Positional, + + /// Boolean flag with no associated value (e.g., `--verbose`). + Flag, + + /// Accepts a single value (e.g., `--name Alice`). + Single, + + /// Accepts multiple values (e.g., `--file a.txt --file b.txt`). + Multi, +} + +impl PickerArgAttr { + /// Determines if the given `PickerArg` represents a positional parameter. + /// + /// If the flag is positional (determined by `flag.is_positional()`), returns + /// `PickerArgAttr::Positional`. Otherwise, invokes the `other` closure to + /// produce and return a `PickerArgAttr`. + /// + /// # Parameters + /// + /// - `flag`: A reference to the [`PickerArg`] to evaluate. + /// - `other`: A closure that returns a [`PickerArgAttr`] when the flag is + /// **not** positional. + #[inline(always)] + pub fn positional_or_else<'a, T>( + flag: &PickerArg<'a, T>, + other: fn() -> PickerArgAttr, + ) -> PickerArgAttr + where + T: Pickable<'a>, + { + if flag.is_positional() { + PickerArgAttr::Positional + } else { + other() + } + } + + /// Determines if the given `PickerArg` represents a positional parameter and returns + /// `PickerArgAttr::Positional` if so. Otherwise, returns the provided `default` attribute. + /// + /// # Parameters + /// + /// - `flag`: A reference to the [`PickerArg`] to evaluate. + /// - `default`: The [`PickerArgAttr`] to return if the flag is not positional. + #[inline(always)] + pub fn positional_or<'a, T>(flag: &PickerArg<'a, T>, default: PickerArgAttr) -> PickerArgAttr + where + T: Pickable<'a>, + { + if flag.is_positional() { + PickerArgAttr::Positional + } else { + default + } + } + + /// Determines if the given `PickerArg` represents a positional parameter and returns + /// `PickerArgAttr::Positional` if so. Otherwise, returns `PickerArgAttr::Single`. + /// + /// # Parameters + /// + /// - `flag`: A reference to the [`PickerArg`] to evaluate. + #[inline(always)] + pub fn positional_or_single<'a, T>(flag: &PickerArg<'a, T>) -> PickerArgAttr + where + T: Pickable<'a>, + { + if flag.is_positional() { + PickerArgAttr::Positional + } else { + PickerArgAttr::Single + } + } + + /// Determines if the given `PickerArg` represents a positional parameter and returns + /// `PickerArgAttr::PositionalMulti` if so. Otherwise, returns `PickerArgAttr::Multi`. + /// + /// # Parameters + /// + /// - `flag`: A reference to the [`PickerArg`] to evaluate. + #[inline(always)] + pub fn positional_or_multi<'a, T>(flag: &PickerArg<'a, T>) -> PickerArgAttr + where + T: Pickable<'a>, + { + if flag.is_positional() { + PickerArgAttr::PositionalMulti + } else { + PickerArgAttr::Multi + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_picker_flag_attr_ordering() { + // Multi > Single > Flag > Positional > PositionalMulti + assert!(PickerArgAttr::Multi > PickerArgAttr::Single); + assert!(PickerArgAttr::Multi > PickerArgAttr::Flag); + assert!(PickerArgAttr::Multi > PickerArgAttr::Positional); + assert!(PickerArgAttr::Multi > PickerArgAttr::PositionalMulti); + + assert!(PickerArgAttr::Single > PickerArgAttr::Flag); + assert!(PickerArgAttr::Single > PickerArgAttr::Positional); + assert!(PickerArgAttr::Single > PickerArgAttr::PositionalMulti); + + assert!(PickerArgAttr::Flag > PickerArgAttr::Positional); + assert!(PickerArgAttr::Flag > PickerArgAttr::PositionalMulti); + + assert!(PickerArgAttr::Positional > PickerArgAttr::PositionalMulti); + + // PartialOrd + assert!(PickerArgAttr::Multi >= PickerArgAttr::Single); + assert!(PickerArgAttr::Single >= PickerArgAttr::Flag); + assert!(PickerArgAttr::Flag >= PickerArgAttr::Positional); + assert!(PickerArgAttr::Positional >= PickerArgAttr::PositionalMulti); + + assert!(PickerArgAttr::PositionalMulti < PickerArgAttr::Positional); + assert!(PickerArgAttr::Positional < PickerArgAttr::Flag); + assert!(PickerArgAttr::Flag < PickerArgAttr::Single); + assert!(PickerArgAttr::Single < PickerArgAttr::Multi); + } + + #[test] + fn test_picker_flag_attr_sorting() { + // Sort + let mut values = vec![ + PickerArgAttr::Flag, + PickerArgAttr::Single, + PickerArgAttr::Positional, + PickerArgAttr::Multi, + PickerArgAttr::PositionalMulti, + ]; + values.sort(); + assert_eq!( + values, + vec![ + PickerArgAttr::PositionalMulti, + PickerArgAttr::Positional, + PickerArgAttr::Flag, + PickerArgAttr::Single, + PickerArgAttr::Multi, + ] + ); + } +} diff --git a/mingling_picker/src/builtin.rs b/mingling_picker/src/builtin.rs new file mode 100644 index 0000000..e855b08 --- /dev/null +++ b/mingling_picker/src/builtin.rs @@ -0,0 +1,4 @@ +mod pick_bool; +mod pick_flag; +mod pick_numbers; +mod pick_string; diff --git a/mingling_picker/src/builtin/pick_bool.rs b/mingling_picker/src/builtin/pick_bool.rs new file mode 100644 index 0000000..ccc4424 --- /dev/null +++ b/mingling_picker/src/builtin/pick_bool.rs @@ -0,0 +1,22 @@ +use crate::parselib::{FlagMatcher, Matcher}; +use crate::pickable_needed::*; + +impl<'a> Pickable<'a> for bool { + fn get_attr(_: &'a PickerArg<'a, Self>) -> PickerArgAttr { + PickerArgAttr::Flag + } + + fn tag(ctx: TagPhaseContext) -> Vec<usize> { + FlagMatcher::match_all(ctx.into()) + } + + fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> { + if raw_strs.is_empty() { + // No matching flag found — signal NotFound so the fallback chain + // (default → route) gets a chance to run. + PickerArgResult::NotFound + } else { + PickerArgResult::Parsed(true) + } + } +} diff --git a/mingling_picker/src/builtin/pick_flag.rs b/mingling_picker/src/builtin/pick_flag.rs new file mode 100644 index 0000000..b642a9a --- /dev/null +++ b/mingling_picker/src/builtin/pick_flag.rs @@ -0,0 +1,21 @@ +use crate::parselib::{FlagMatcher, Matcher}; +use crate::pickable_needed::*; +use crate::value::Flag; + +impl<'a> Pickable<'a> for Flag { + fn get_attr(_: &'a PickerArg<'a, Self>) -> PickerArgAttr { + PickerArgAttr::Flag + } + + fn tag(ctx: TagPhaseContext) -> Vec<usize> { + FlagMatcher::match_all(ctx.into()) + } + + fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> { + if raw_strs.is_empty() { + PickerArgResult::Parsed(Flag::Inactive) + } else { + PickerArgResult::Parsed(Flag::Active) + } + } +} diff --git a/mingling_picker/src/builtin/pick_numbers.rs b/mingling_picker/src/builtin/pick_numbers.rs new file mode 100644 index 0000000..a5ab0a9 --- /dev/null +++ b/mingling_picker/src/builtin/pick_numbers.rs @@ -0,0 +1,80 @@ +use crate::{BoundaryCheck, SinglePickable, pickable_needed::*}; + +macro_rules! impl_single_pickable_num { + ($($t:ty),+ $(,)?) => { + $(impl SinglePickable for $t { + fn pick_single(str: Option<&str>) -> PickerArgResult<Self> { + match str { + Some(s) => s.parse::<$t>().map(PickerArgResult::Parsed).unwrap_or(PickerArgResult::NotFound), + None => PickerArgResult::NotFound, + } + } + })+ + }; +} + +/// Returns `true` if `raw` looks like an integer (digits, optional `+`/`-` prefix, +/// no decimal point or exponent). +fn is_int_like(raw: &str) -> bool { + let s = raw.trim(); + let bytes = s.as_bytes(); + if bytes.is_empty() { + return false; + } + let mut i = 0; + if bytes[0] == b'-' || bytes[0] == b'+' { + i = 1; + } + if i >= bytes.len() { + return false; + } + for &b in &bytes[i..] { + if !b.is_ascii_digit() { + return false; + } + } + true +} + +/// Returns `true` if `raw` looks like a float (contains `.`, `e`, or `E`). +fn is_float_like(raw: &str) -> bool { + let s = raw.trim(); + s.contains('.') || s.contains('e') || s.contains('E') +} + +impl_single_pickable_num! { + i8, i16, i32, i64, i128, isize, + u8, u16, u32, u64, u128, usize, + f32, f64, +} + +// Integer boundary: only accept strings that look like integers. +// Float-like strings trigger a boundary. +macro_rules! impl_boundary_check_int { + ($($t:ty),+ $(,)?) => { + $(impl BoundaryCheck for $t { + fn check_boundary(raw: &str) -> bool { + !is_int_like(raw) + } + })+ + }; +} + +impl_boundary_check_int! { + i8, i16, i32, i64, i128, isize, + u8, u16, u32, u64, u128, usize, +} + +// Float boundary: only accept strings that look like floats. +// Integer-like strings trigger a boundary. +impl BoundaryCheck for f32 { + fn check_boundary(raw: &str) -> bool { + !is_float_like(raw) || raw.parse::<f32>().is_err() + } +} + +impl BoundaryCheck for f64 { + fn check_boundary(raw: &str) -> bool { + !is_float_like(raw) || raw.parse::<f64>().is_err() + } +} diff --git a/mingling_picker/src/builtin/pick_string.rs b/mingling_picker/src/builtin/pick_string.rs new file mode 100644 index 0000000..c96f667 --- /dev/null +++ b/mingling_picker/src/builtin/pick_string.rs @@ -0,0 +1,11 @@ +use crate::PickerArgResult::NotFound; +use crate::{SinglePickable, pickable_needed::*}; + +impl SinglePickable for String { + fn pick_single(str: Option<&str>) -> PickerArgResult<Self> { + match str { + Some(str) => PickerArgResult::Parsed(str.to_string()), + None => NotFound, + } + } +} diff --git a/mingling_picker/src/corebind.rs b/mingling_picker/src/corebind.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/mingling_picker/src/corebind.rs @@ -0,0 +1 @@ + diff --git a/mingling_picker/src/infos.rs b/mingling_picker/src/infos.rs new file mode 100644 index 0000000..d2a0fce --- /dev/null +++ b/mingling_picker/src/infos.rs @@ -0,0 +1,446 @@ +use crate::{Pickable, PickerArg}; + +/// Represents the result of parsing or looking up a value. +/// +/// This enum is generic over the type being parsed. It models three possible outcomes: +/// - [`Unparsed`](PickerArgResult::Unparsed): The value has not yet been parsed (default). +/// - [`Parsed`](PickerArgResult::Parsed): The value was successfully parsed into `Type`. +/// - [`NotFound`](PickerArgResult::NotFound): The requested value could not be found. +#[derive(Default)] +pub enum PickerArgResult<Type> { + /// The value has not yet been parsed (default). + #[default] + Unparsed, + + /// The value was successfully parsed into `Type`. + Parsed(Type), + + /// The requested value could not be found. + NotFound, +} + +impl<Type, E> From<Result<Type, E>> for PickerArgResult<Type> { + /// Converts a `Result<Type, E>` into a `PickerArgResult<Type>`. + /// + /// - `Ok(value)` maps to [`Parsed(value)`](PickerArgResult::Parsed). + /// - `Err(_)` maps to [`NotFound`](PickerArgResult::NotFound). + fn from(result: Result<Type, E>) -> Self { + match result { + Ok(value) => PickerArgResult::Parsed(value), + Err(_) => PickerArgResult::NotFound, + } + } +} + +impl<Type> From<Option<Type>> for PickerArgResult<Type> { + /// Converts an `Option<Type>` into a `PickerArgResult<Type>`. + /// + /// - `Some(value)` maps to [`Parsed(value)`](PickerArgResult::Parsed). + /// - `None` maps to [`NotFound`](PickerArgResult::NotFound). + fn from(option: Option<Type>) -> Self { + match option { + Some(value) => PickerArgResult::Parsed(value), + None => PickerArgResult::NotFound, + } + } +} + +impl<Type> PickerArgResult<Type> { + /// Returns `true` if the result is [`Parsed`](PickerArgResult::Parsed). + /// + /// # Examples + /// + /// ``` + /// use mingling_picker::PickerArgResult; + /// + /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42); + /// assert!(result.is_parsed()); + /// + /// let result: PickerArgResult<i32> = PickerArgResult::NotFound; + /// assert!(!result.is_parsed()); + /// ``` + pub fn is_parsed(&self) -> bool { + matches!(self, PickerArgResult::Parsed(_)) + } + + /// Returns `true` if the result is [`Parsed`](PickerArgResult::Parsed) or [`NotFound`](PickerArgResult::NotFound). + /// i.e., the value exists (was either found or not yet parsed). + /// Typically indicates the value was "found" in some sense. + /// + /// # Examples + /// + /// ``` + /// use mingling_picker::PickerArgResult; + /// + /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42); + /// assert!(result.is_found()); + /// + /// let result: PickerArgResult<i32> = PickerArgResult::NotFound; + /// assert!(result.is_found()); + /// ``` + pub fn is_found(&self) -> bool { + matches!(self, PickerArgResult::Parsed(_) | PickerArgResult::NotFound) + } + + /// Returns `true` if the result is [`Unparsed`](PickerArgResult::Unparsed) or [`NotFound`](PickerArgResult::NotFound). + /// + /// # Examples + /// + /// ``` + /// use mingling_picker::PickerArgResult; + /// + /// let result: PickerArgResult<i32> = PickerArgResult::Unparsed; + /// assert!(result.is_err()); + /// + /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(10); + /// assert!(!result.is_err()); + /// ``` + pub fn is_err(&self) -> bool { + !matches!(self, PickerArgResult::Parsed(_)) + } + + /// Returns `Some(&Type)` if [`Parsed`](PickerArgResult::Parsed), otherwise `None`. + /// + /// # Examples + /// + /// ``` + /// use mingling_picker::PickerArgResult; + /// + /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42); + /// assert_eq!(result.parsed(), Some(&42)); + /// + /// let result: PickerArgResult<i32> = PickerArgResult::NotFound; + /// assert_eq!(result.parsed(), None); + /// ``` + pub fn parsed(&self) -> Option<&Type> { + if let PickerArgResult::Parsed(value) = self { + Some(value) + } else { + None + } + } + + /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or panics with a given message. + /// + /// # Panics + /// Panics if the value is not [`Parsed`](PickerArgResult::Parsed), with a message including the provided `msg`. + /// + /// # Examples + /// + /// ```should_panic + /// use mingling_picker::PickerArgResult; + /// + /// let result: PickerArgResult<i32> = PickerArgResult::NotFound; + /// result.expect("expected a parsed value"); + /// ``` + pub fn expect(self, msg: &str) -> Type { + match self { + PickerArgResult::Parsed(value) => value, + _ => panic!("{}", msg), + } + } + + /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or panics. + /// + /// # Panics + /// Panics if the value is not [`Parsed`](PickerArgResult::Parsed). + /// + /// # Examples + /// + /// ``` + /// use mingling_picker::PickerArgResult; + /// + /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42); + /// assert_eq!(result.unwrap(), 42); + /// ``` + /// + /// ```should_panic + /// use mingling_picker::PickerArgResult; + /// + /// let result: PickerArgResult<i32> = PickerArgResult::NotFound; + /// result.unwrap(); + /// ``` + pub fn unwrap(self) -> Type { + match self { + PickerArgResult::Parsed(value) => value, + PickerArgResult::Unparsed => { + panic!("called `PickerArgResult::unwrap()` on an `Unparsed` value") + } + PickerArgResult::NotFound => { + panic!("called `PickerArgResult::unwrap()` on a `NotFound` value") + } + } + } + + /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or a provided `default`. + /// + /// # Examples + /// + /// ``` + /// use mingling_picker::PickerArgResult; + /// + /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42); + /// assert_eq!(result.unwrap_or(0), 42); + /// + /// let result: PickerArgResult<i32> = PickerArgResult::NotFound; + /// assert_eq!(result.unwrap_or(0), 0); + /// ``` + pub fn unwrap_or(self, default: Type) -> Type { + match self { + PickerArgResult::Parsed(value) => value, + _ => default, + } + } + + /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or computes it from a closure. + /// + /// # Examples + /// + /// ``` + /// use mingling_picker::PickerArgResult; + /// + /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42); + /// assert_eq!(result.unwrap_or_else(|| 0), 42); + /// + /// let result: PickerArgResult<i32> = PickerArgResult::NotFound; + /// assert_eq!(result.unwrap_or_else(|| 0), 0); + /// ``` + pub fn unwrap_or_else<F: FnOnce() -> Type>(self, f: F) -> Type { + match self { + PickerArgResult::Parsed(value) => value, + _ => f(), + } + } + + /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or the default value of `Type`. + /// + /// # Examples + /// + /// ``` + /// use mingling_picker::PickerArgResult; + /// + /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42); + /// assert_eq!(result.unwrap_or_default(), 42); + /// + /// let result: PickerArgResult<i32> = PickerArgResult::NotFound; + /// assert_eq!(result.unwrap_or_default(), 0); + /// ``` + pub fn unwrap_or_default(self) -> Type + where + Type: Default, + { + match self { + PickerArgResult::Parsed(value) => value, + _ => Type::default(), + } + } + + /// Converts `PickerArgResult<Type>` into `Option<Type>`. + /// + /// Returns `Some(Type)` if [`Parsed`](PickerArgResult::Parsed), otherwise `None`. + /// + /// # Examples + /// + /// ``` + /// use mingling_picker::PickerArgResult; + /// + /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42); + /// assert_eq!(result.to_option(), Some(42)); + /// + /// let result: PickerArgResult<i32> = PickerArgResult::NotFound; + /// assert_eq!(result.to_option(), None); + /// + /// let result: PickerArgResult<i32> = PickerArgResult::Unparsed; + /// assert_eq!(result.to_option(), None); + /// ``` + pub fn to_option(self) -> Option<Type> { + match self { + PickerArgResult::Parsed(value) => Some(value), + _ => None, + } + } +} + +/// Represents metadata about a command-line argument or flag. +/// +/// This struct stores all relevant information about a tag/argument that can be used +/// for parsing command-line inputs. It includes the short form (e.g., `-n`), long form +/// (e.g., `--name`), aliases, and various flags that control parsing behavior. +pub struct PickerArgInfo<'a> { + /// The short form of the tag, e.g. `'n'` for `-n`. + pub short: Option<char>, + /// The long form of the tag, e.g. `"name"` for `--name`. + pub long: Option<&'a str>, + /// Alternative names for the tag, e.g. `["-N", "--nickname"]`. + pub alias: Option<Vec<&'a str>>, + /// Whether this tag is a positional argument (no `-` or `--` prefix). + pub positional: bool, + /// Whether this tag is optional or required. + pub optional: bool, + /// Whether this tag can accept multiple values. + pub multi: bool, + /// Whether this tag participates in parsing after a `--` separator. + pub is_flag: bool, +} + +impl<'a, T> From<PickerArg<'a, T>> for PickerArgInfo<'a> +where + T: Pickable<'a>, +{ + fn from(value: PickerArg<'a, T>) -> Self { + let (long, alias) = match value.full.len() { + 0 => (None, None), + _ => { + let long = Some(value.full[0]); + let alias = if value.full.len() > 1 { + Some(value.full[1..].to_vec()) + } else { + None + }; + (long, alias) + } + }; + + Self { + short: value.short, + long, + alias, + positional: value.positional, + optional: false, + multi: false, + is_flag: false, + } + } +} + +impl<'a, T: Pickable<'a>> From<&'a PickerArg<'a, T>> for PickerArgInfo<'a> { + fn from(value: &'a PickerArg<'a, T>) -> Self { + let (long, alias) = match value.full.len() { + 0 => (None, None), + _ => { + let long = Some(value.full[0]); + let alias = if value.full.len() > 1 { + Some(value.full[1..].to_vec()) + } else { + None + }; + (long, alias) + } + }; + + Self { + short: value.short, + long, + alias, + positional: value.positional, + optional: false, + multi: false, + is_flag: false, + } + } +} + +impl<'a> PickerArgInfo<'a> { + /// Create a new `PickerTag` with default values. + pub fn new() -> Self { + Self { + short: None, + long: None, + alias: None, + positional: false, + optional: false, + multi: false, + is_flag: false, + } + } + + /// Set the short flag (e.g., `'n'` for `-n`). + pub fn with_short(mut self, short: char) -> Self { + self.short = Some(short); + self + } + + /// Set the long flag (e.g., `"name"` for `--name`). + pub fn with_long(mut self, long: &'a str) -> Self { + self.long = Some(long); + self + } + + /// Set aliases for the tag. + pub fn with_alias(mut self, alias: Vec<&'a str>) -> Self { + self.alias = Some(alias); + self + } + + /// Mark the tag as positional. + pub fn with_positional(mut self, positional: bool) -> Self { + self.positional = positional; + self + } + + /// Mark the tag as optional. + pub fn with_optional(mut self, optional: bool) -> Self { + self.optional = optional; + self + } + + /// Mark the tag as multi-value. + pub fn with_multi(mut self, multi: bool) -> Self { + self.multi = multi; + self + } + + /// Mark the tag as a flag that participates in parsing after `--`. + pub fn with_is_flag(mut self, is_flag: bool) -> Self { + self.is_flag = is_flag; + self + } + + /// Set the short flag (e.g., `'n'` for `-n`). + pub fn set_short(&mut self, short: char) -> &mut Self { + self.short = Some(short); + self + } + + /// Set the long flag (e.g., `"name"` for `--name`). + pub fn set_long(&mut self, long: &'a str) -> &mut Self { + self.long = Some(long); + self + } + + /// Set aliases for the tag. + pub fn set_alias(&mut self, alias: Vec<&'a str>) -> &mut Self { + self.alias = Some(alias); + self + } + + /// Set whether this tag is positional. + pub fn set_positional(&mut self, positional: bool) -> &mut Self { + self.positional = positional; + self + } + + /// Set whether this tag is optional. + pub fn set_optional(&mut self, optional: bool) -> &mut Self { + self.optional = optional; + self + } + + /// Set whether this tag accepts multiple values. + pub fn set_multi(&mut self, multi: bool) -> &mut Self { + self.multi = multi; + self + } + + /// Set whether this tag participates in parsing after a `--` separator. + pub fn set_is_flag(&mut self, is_flag: bool) -> &mut Self { + self.is_flag = is_flag; + self + } +} + +impl<'a> Default for PickerArgInfo<'a> { + fn default() -> Self { + Self::new() + } +} diff --git a/mingling_picker/src/lib.rs b/mingling_picker/src/lib.rs new file mode 100644 index 0000000..ffa6e13 --- /dev/null +++ b/mingling_picker/src/lib.rs @@ -0,0 +1,57 @@ +#![doc = include_str!("../README.md")] + +mod builtin; + +mod picker; +pub use picker::*; + +mod pickable; +pub use pickable::*; + +mod arg; +pub use arg::*; + +mod infos; +pub use infos::*; + +/// Provides the specific parsing logic for command-line arguments and common utilities, +/// as well as customization of command-line argument styles. +pub mod parselib; + +/// Parser-provided parseable command-line types +pub mod value; + +/// The prelude module, which re-exports the most commonly used traits and types. +/// +/// This module is intended to be imported with a wildcard import: +/// +/// ```ignore +/// use mingling_picker::prelude::*; +/// ``` +pub mod prelude { + pub use crate::IntoPicker; + pub use crate::macros::arg; +} + +/// Re-export of the `mingling_picker_macros` crate +pub mod macros { + pub use mingling_picker_macros::arg; +} + +/// Provides the types necessary for implementing the `Pickable` trait +pub mod pickable_needed { + pub use crate::{Pickable, PickerArg, PickerArgAttr, PickerArgResult, TagPhaseContext}; +} + +/// Provides the types necessary for implementing the `Matcher` trait +pub mod matcher_needed { + pub use crate::PickerArgInfo; + pub use crate::parselib::{MaskedArg, Matcher, ParserStyle}; +} + +#[cfg(feature = "mingling_support")] +mod corebind; + +#[allow(unused_imports)] +#[cfg(feature = "mingling_support")] +pub use corebind::*; diff --git a/mingling_picker/src/parselib.rs b/mingling_picker/src/parselib.rs new file mode 100644 index 0000000..7fbd606 --- /dev/null +++ b/mingling_picker/src/parselib.rs @@ -0,0 +1,144 @@ +mod flag_matcher; +pub use flag_matcher::*; + +mod arg_matcher; +pub use arg_matcher::*; + +mod multi_arg_matcher; +pub use multi_arg_matcher::*; + +mod pos_matcher; +pub use pos_matcher::*; + +mod single_matcher; +pub use single_matcher::*; + +mod style; +pub use style::*; + +mod utils; +pub use utils::*; + +use crate::{PickerArgInfo, PickerArgs}; + +/// Represents a single argument with its original raw string and index. +/// +/// This is used during pattern matching to provide context about +/// which argument is being processed. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct MaskedArg<'a> { + /// The raw string value of the argument. + pub raw: &'a str, + /// The original index of the argument in the full argument list. + pub raw_idx: usize, +} + +/// Trait for defining matching logic against masked arguments. +/// +/// Implementors can define custom strategies for matching one or all +/// arguments that pass through a mask filter. +pub trait Matcher { + /// Called when only one match is needed. + /// + /// Returns the index of the first matched argument, or `None` if no match. + fn on_match_one( + args: &[MaskedArg], + style: &ParserStyle, + arg_info: &PickerArgInfo, + ) -> Option<usize>; + + /// Called when all matches are needed. + /// + /// Returns a vector of indices of all matched arguments. + fn on_match_all( + args: &[MaskedArg], + style: &ParserStyle, + arg_info: &PickerArgInfo, + ) -> Vec<usize>; + + /// Convenience method that builds masked arguments from `PickerArgs` and a mask, + /// then calls `on_match_one`. + fn match_one<'a>(ctx: MatcherContext<'a>) -> Option<usize> { + let masked_args = build_masked_args(ctx.args, ctx.mask); + Self::on_match_one(masked_args.as_slice(), ctx.style, ctx.arg_info) + } + + /// Convenience method that builds masked arguments from `PickerArgs` and a mask, + /// then calls `on_match_all`. + fn match_all<'a>(ctx: MatcherContext<'a>) -> Vec<usize> { + let masked_args = build_masked_args(ctx.args, ctx.mask); + Self::on_match_all(masked_args.as_slice(), ctx.style, ctx.arg_info) + } +} + +/// Context for matcher operations +/// +/// This struct bundles together the key pieces of data needed during matching: +/// - `args`: The full set of parsed arguments. +/// - `mask`: A byte mask indicating which arguments are currently active (non-zero = active). +/// - `style`: The parsing style configuration. +pub struct MatcherContext<'a> { + /// The full set of parsed arguments. + pub args: &'a PickerArgs<'a>, + + /// A byte mask where non-zero values indicate the argument at that position is active/should be matched. + pub mask: &'a [u8], + + /// The parsing style configuration. + pub style: &'a ParserStyle<'a>, + + /// Metadata about the command-line argument/flag being processed. + /// + /// Contains information such as short form (`-n`), long form (`--name`), + /// aliases, and parsing flags (positional, optional, multi, is_flag). + /// Used by matchers to make decisions based on argument characteristics. + pub arg_info: &'a PickerArgInfo<'a>, +} + +impl<'a> From<&'a crate::TagPhaseContext<'a>> for MatcherContext<'a> { + fn from(ctx: &'a crate::TagPhaseContext<'a>) -> Self { + MatcherContext { + args: ctx.args, + mask: ctx.mask, + style: ParserStyle::global_style(), + arg_info: ctx.arg_info, + } + } +} + +impl<'a> From<crate::TagPhaseContext<'a>> for MatcherContext<'a> { + fn from(ctx: crate::TagPhaseContext<'a>) -> Self { + MatcherContext { + args: ctx.args, + mask: ctx.mask, + style: ParserStyle::global_style(), + arg_info: ctx.arg_info, + } + } +} + +#[inline(always)] +fn is_masked(mask: &[u8], idx: usize) -> bool { + idx < mask.len() && mask[idx] != 0 +} + +#[inline(always)] +fn build_masked_args<'a>(args: &'a PickerArgs, mask: &'a [u8]) -> Vec<MaskedArg<'a>> { + let mut cidx = 0; + args.iter() + .filter_map(|r| { + let idx = cidx; + cidx += 1; + // Include args where mask is 0 (available/not yet claimed). + // mask[i] = 0 means available; mask[i] != 0 means already claimed. + if !is_masked(mask, idx) { + Some(MaskedArg { + raw: r, + raw_idx: idx, + }) + } else { + None + } + }) + .collect() +} diff --git a/mingling_picker/src/parselib/arg_matcher.rs b/mingling_picker/src/parselib/arg_matcher.rs new file mode 100644 index 0000000..38bb9cc --- /dev/null +++ b/mingling_picker/src/parselib/arg_matcher.rs @@ -0,0 +1,142 @@ +use crate::{ + matcher_needed::*, + parselib::{build_possible_flags, seek_end_of_options}, +}; + +/// `ArgMatcher` is used for parameters that carry a single value. +/// +/// It handles two scenarios: +/// +/// **Named** — `--name Alice` or `--name=Alice`. +/// Each flag occurrence consumes **one** following argument as its value, +/// regardless of what it is (even if it looks like a flag). +/// This ensures the mask correctly claims the value slot; validation is +/// the `Pickable`'s responsibility. +/// +/// **Positional** — no flag prefix, matched by position. +/// +/// # Examples +/// +/// | Input | `on_match_one` | `on_match_all` | +/// |-------|----------------|----------------| +/// | `--name Alice` | `[0, 1]` (via Pickable tag) | `[0, 1]` | +/// | `--name=Alice` | `[0]` | `[0]` | +/// | `--val a --val b` | `[0, 1]` | `[0, 1, 2, 3]` | +/// +/// Args after `--` are ignored. +pub struct ArgMatcher; + +impl ArgMatcher { + /// Check whether `raw` matches `flag_str`, optionally with an inline value + /// separated by the style's value separator (`=` for Unix, `:` for PowerShell). + #[inline(always)] + fn matches(raw: &str, flag_str: &str, case_sensitive: bool, sep: char) -> bool { + let eq_match = + |r: &str, f: &str| r.len() > f.len() && r.as_bytes().get(f.len()) == Some(&(sep as u8)); + + if case_sensitive { + raw == flag_str || (raw.starts_with(flag_str) && eq_match(raw, flag_str)) + } else { + raw.eq_ignore_ascii_case(flag_str) + || (raw.len() > flag_str.len() + && raw[..flag_str.len()].eq_ignore_ascii_case(flag_str) + && raw.as_bytes()[flag_str.len()] == sep as u8) + } + } + + /// Check whether the argument contains its value inline via the style's + /// value separator (eq mode), so no extra mask slot is needed. + #[inline(always)] + fn is_inline_value(raw: &str, flag_str: &str, sep: char) -> bool { + raw.len() > flag_str.len() && raw.as_bytes().get(flag_str.len()) == Some(&(sep as u8)) + } +} + +impl Matcher for ArgMatcher { + fn on_match_one( + args: &[MaskedArg], + style: &ParserStyle, + arg_info: &PickerArgInfo, + ) -> Option<usize> { + if arg_info.positional { + return args.first().map(|a| a.raw_idx); + } + + let possible_flags = build_possible_flags(style, arg_info); + let end = seek_end_of_options(args, style); + let sep = style.value_separator; + + for arg in args { + if end.is_some_and(|e| arg.raw_idx >= e) { + break; + } + + let matched = possible_flags + .iter() + .any(|f| Self::matches(arg.raw, f, style.case_sensitive, sep)); + if matched { + return Some(arg.raw_idx); + } + } + + None + } + + fn on_match_all( + args: &[MaskedArg], + style: &ParserStyle, + arg_info: &PickerArgInfo, + ) -> Vec<usize> { + if arg_info.positional { + let end = seek_end_of_options(args, style); + return args + .iter() + .take_while(|a| end.is_none_or(|e| a.raw_idx < e)) + .map(|a| a.raw_idx) + .collect(); + } + + let possible_flags = build_possible_flags(style, arg_info); + let end = seek_end_of_options(args, style); + let sep = style.value_separator; + + let mut result = Vec::new(); + let mut i = 0; + while i < args.len() { + if end.is_some_and(|e| args[i].raw_idx >= e) { + break; + } + + let matched = possible_flags + .iter() + .any(|f| Self::matches(args[i].raw, f, style.case_sensitive, sep)); + + if matched { + let flag_str = possible_flags + .iter() + .find(|f| Self::matches(args[i].raw, f, style.case_sensitive, sep)) + .expect("already matched"); + + result.push(args[i].raw_idx); + + if !Self::is_inline_value(args[i].raw, flag_str, sep) { + if i + 1 < args.len() + // Don't consume `--` (end-of-options marker) as a value. + && end.is_none_or(|e| args[i + 1].raw_idx < e) + { + result.push(args[i + 1].raw_idx); + i += 2; + continue; + } + i += 1; + continue; + } + i += 1; + continue; + } + i += 1; + } + + result + } +} diff --git a/mingling_picker/src/parselib/flag_matcher.rs b/mingling_picker/src/parselib/flag_matcher.rs new file mode 100644 index 0000000..e93d35a --- /dev/null +++ b/mingling_picker/src/parselib/flag_matcher.rs @@ -0,0 +1,82 @@ +use crate::{ + matcher_needed::*, + parselib::{build_possible_flags, get_seeked_first, multi_seek_eq, seek_end_of_options}, +}; + +/// `FlagMatcher` is used to match flags in command-line arguments. +/// +/// Flags typically start with `-` or `--` (e.g., `-h`, `--help`), +/// and do not carry additional values. This matcher is responsible for finding +/// these flags in the argument list, taking into account that flags after `--` +/// (end-of-options marker) should not be matched. +pub struct FlagMatcher; + +impl Matcher for FlagMatcher { + fn on_match_one( + args: &[MaskedArg], + style: &ParserStyle, + arg_info: &PickerArgInfo, + ) -> Option<usize> { + let possible_flags = build_possible_flags(style, arg_info); + let flag_refs: Vec<&str> = possible_flags.iter().map(|s| s.as_str()).collect(); + let end_of_options = seek_end_of_options(args, style); + + let result = get_seeked_first(multi_seek_eq(args, &flag_refs, style.case_sensitive)); + + match (end_of_options, result) { + (Some(end), Some(current)) if current > end => None, + _ => result, + } + } + + fn on_match_all( + args: &[MaskedArg], + style: &ParserStyle, + arg_info: &PickerArgInfo, + ) -> Vec<usize> { + let possible_flags = build_possible_flags(style, arg_info); + single_pass_match_all(args, style, &possible_flags) + } +} + +/// Single-pass match: finds the `--` marker and matching flags in one iteration. +fn single_pass_match_all( + args: &[MaskedArg], + style: &ParserStyle, + possible_flags: &[String], +) -> Vec<usize> { + let flag_refs: Vec<&str> = possible_flags.iter().map(|s| s.as_str()).collect(); + let eoo = style.end_of_options; + let case_sensitive = style.case_sensitive; + + let mut end_pos: Option<usize> = None; + let mut matches: Vec<usize> = Vec::new(); + + for arg in args { + if end_pos.is_none() { + let is_eoo = if case_sensitive { + arg.raw == eoo + } else { + arg.raw.eq_ignore_ascii_case(eoo) + }; + if is_eoo { + end_pos = Some(arg.raw_idx); + continue; + } + } + + // Only match flags before the end-of-options marker. + if end_pos.is_none() { + let matched = if case_sensitive { + flag_refs.contains(&arg.raw) + } else { + flag_refs.iter().any(|s| arg.raw.eq_ignore_ascii_case(s)) + }; + if matched { + matches.push(arg.raw_idx); + } + } + } + + matches +} diff --git a/mingling_picker/src/parselib/multi_arg_matcher.rs b/mingling_picker/src/parselib/multi_arg_matcher.rs new file mode 100644 index 0000000..748b1be --- /dev/null +++ b/mingling_picker/src/parselib/multi_arg_matcher.rs @@ -0,0 +1,141 @@ +use crate::{ + matcher_needed::*, + parselib::{build_possible_flags, seek_end_of_options}, +}; + +/// `MultiArgMatcher` matches a named flag and **all** consecutive arguments +/// that follow it, stopping at the next flag, the `--` marker, or the end +/// of the argument list. +/// +/// This is the tag implementation for `Multi` and `GreedyMulti` types +/// such as `Vec<String>` (`--files a.txt b.txt`). +/// +/// # Behavior +/// +/// | Input | `on_match_all` | +/// |-------|----------------| +/// | `--val a b --val d e` | `[0, 1, 2, 5, 6]` (two groups) | +/// | `--val=1 2` | `[0, 1]` (eq mode + one extra value) | +/// +/// Args after `--` are ignored. +pub struct MultiArgMatcher; + +impl Matcher for MultiArgMatcher { + fn on_match_one( + args: &[MaskedArg], + style: &ParserStyle, + arg_info: &PickerArgInfo, + ) -> Option<usize> { + if arg_info.positional { + return args.first().map(|a| a.raw_idx); + } + + let possible_flags = build_possible_flags(style, arg_info); + let end = seek_end_of_options(args, style); + let sep = style.value_separator; + + for arg in args { + if end.is_some_and(|e| arg.raw_idx >= e) { + break; + } + let matched = possible_flags + .iter() + .any(|f| Self::flag_match(arg.raw, f, style.case_sensitive, sep)); + if matched { + return Some(arg.raw_idx); + } + } + None + } + + fn on_match_all( + args: &[MaskedArg], + style: &ParserStyle, + arg_info: &PickerArgInfo, + ) -> Vec<usize> { + if arg_info.positional { + let end = seek_end_of_options(args, style); + return args + .iter() + .take_while(|a| end.is_none_or(|e| a.raw_idx < e)) + .map(|a| a.raw_idx) + .collect(); + } + + let possible_flags = build_possible_flags(style, arg_info); + let end = seek_end_of_options(args, style); + let sep = style.value_separator; + let is_flag = + |raw: &str| raw.starts_with(style.long_prefix) || raw.starts_with(style.short_prefix); + let is_our_flag = |raw: &str| { + possible_flags + .iter() + .any(|f| Self::flag_match(raw, f, style.case_sensitive, sep)) + }; + + let mut result = Vec::new(); + let mut i = 0; + while i < args.len() { + if end.is_some_and(|e| args[i].raw_idx >= e) { + break; + } + + let matched = is_our_flag(args[i].raw); + + if matched { + result.push(args[i].raw_idx); + + if Self::is_eq_match(args[i].raw, &possible_flags, style.case_sensitive, sep) { + i += 1; + while i < args.len() + && end.is_none_or(|e| args[i].raw_idx < e) + && !is_flag(args[i].raw) + { + result.push(args[i].raw_idx); + i += 1; + } + continue; + } + + i += 1; + while i < args.len() + && end.is_none_or(|e| args[i].raw_idx < e) + && !is_flag(args[i].raw) + { + result.push(args[i].raw_idx); + i += 1; + } + continue; + } + i += 1; + } + + result + } +} + +impl MultiArgMatcher { + #[inline(always)] + fn flag_match(raw: &str, flag_str: &str, case_sensitive: bool, sep: char) -> bool { + let eq = + |r: &str, f: &str| r.len() > f.len() && r.as_bytes().get(f.len()) == Some(&(sep as u8)); + + if case_sensitive { + raw == flag_str || (raw.starts_with(flag_str) && eq(raw, flag_str)) + } else { + raw.eq_ignore_ascii_case(flag_str) + || (raw.len() > flag_str.len() + && raw[..flag_str.len()].eq_ignore_ascii_case(flag_str) + && raw.as_bytes()[flag_str.len()] == sep as u8) + } + } + + #[inline(always)] + fn is_eq_match(raw: &str, flags: &[String], case_sensitive: bool, sep: char) -> bool { + flags.iter().any(|f| { + Self::flag_match(raw, f, case_sensitive, sep) + && raw.len() > f.len() + && raw.as_bytes().get(f.len()) == Some(&(sep as u8)) + }) + } +} diff --git a/mingling_picker/src/parselib/pos_matcher.rs b/mingling_picker/src/parselib/pos_matcher.rs new file mode 100644 index 0000000..279e01e --- /dev/null +++ b/mingling_picker/src/parselib/pos_matcher.rs @@ -0,0 +1,71 @@ +use crate::{matcher_needed::*, parselib::seek_end_of_options}; + +/// `PositionalMatcher` matches positional arguments — values not associated +/// with any named flag. +/// +/// # Rules +/// +/// * Before `--`: skips any argument that starts with the style's long or short +/// prefix (those belong to named matchers). +/// * After `--`: takes **everything** — the `--` marker signals that all +/// remaining values are positional, even if they look like flags. +/// * Runs at the lowest priority (see [`PickerArgAttr::Positional`](crate::PickerArgAttr::Positional)). +pub struct PositionalMatcher; + +impl PositionalMatcher { + /// Check whether `raw` looks like a named flag (starts with a prefix). + #[inline(always)] + fn is_flag_like(raw: &str, style: &ParserStyle) -> bool { + raw.starts_with(style.long_prefix) || raw.starts_with(style.short_prefix) + } +} + +impl Matcher for PositionalMatcher { + fn on_match_one( + args: &[MaskedArg], + style: &ParserStyle, + _arg_info: &PickerArgInfo, + ) -> Option<usize> { + let end = seek_end_of_options(args, style); + + for arg in args { + if end.is_some_and(|e| arg.raw_idx == e) { + // Hit `--`: everything from here on is positional, + // including the first arg after `--`. + continue; + } + if end.is_some_and(|e| arg.raw_idx > e) { + // After `--`: accept everything. + return Some(arg.raw_idx); + } + // Before `--`: skip flag-like args. + if !Self::is_flag_like(arg.raw, style) { + return Some(arg.raw_idx); + } + } + + None + } + + fn on_match_all( + args: &[MaskedArg], + style: &ParserStyle, + _arg_info: &PickerArgInfo, + ) -> Vec<usize> { + let end = seek_end_of_options(args, style); + let mut after_end = false; + let mut result = Vec::new(); + + for arg in args { + if end.is_some_and(|e| arg.raw_idx == e) { + after_end = true; + continue; + } + if after_end || !Self::is_flag_like(arg.raw, style) { + result.push(arg.raw_idx); + } + } + + result + } +} diff --git a/mingling_picker/src/parselib/single_matcher.rs b/mingling_picker/src/parselib/single_matcher.rs new file mode 100644 index 0000000..25c4741 --- /dev/null +++ b/mingling_picker/src/parselib/single_matcher.rs @@ -0,0 +1,59 @@ +use crate::TagPhaseContext; +use crate::parselib::{ArgMatcher, Matcher, ParserStyle, PositionalMatcher}; + +/// `SingleMatcher` is a composite matcher for single-value parameters. +/// +/// It delegates to [`PositionalMatcher`] for positional args and +/// [`ArgMatcher`] for named args, adding a guard: if a named flag +/// captures only itself with no inline value (eq mode), the result +/// is cleared so that [`Pickable::pick`](crate::Pickable::pick) receives `[]` → `NotFound`. +/// +/// This is the standard tag implementation for all `Single`-type +/// `Pickable` implementations (e.g., `String`, `i32`, `u64`). +pub struct SingleMatcher; + +impl SingleMatcher { + /// Match a single positional value or a named flag+value pair. + /// + /// For named args, only complete pairs (flag + value) are kept. + /// Flag occurrences without a following value or inline separator + /// are dropped so they remain available for other matchers. + #[inline(always)] + pub fn tag(ctx: TagPhaseContext) -> Vec<usize> { + if ctx.arg_info.positional { + PositionalMatcher::match_one(ctx.into()) + .map(|i| vec![i]) + .unwrap_or_default() + } else { + let args = ctx.args; + let positions = ArgMatcher::match_all(ctx.into()); + let sep = ParserStyle::global_style().value_separator; + + // Walk pairs: [flag, value, flag, value, ...] + // Drop any flag that has no following value and no inline separator. + let mut i = 0; + let mut result = Vec::with_capacity(positions.len()); + while i < positions.len() { + let flag_idx = positions[i]; + if let Some(raw) = args.get(flag_idx) + && raw.contains(sep) + { + // Eq mode: value is inline, keep just the flag. + result.push(flag_idx); + i += 1; + continue; + } + if i + 1 < positions.len() { + // Pair: flag + value. + result.push(flag_idx); + result.push(positions[i + 1]); + i += 2; + } else { + // Flag without value — drop it. + i += 1; + } + } + result + } + } +} diff --git a/mingling_picker/src/parselib/style.rs b/mingling_picker/src/parselib/style.rs new file mode 100644 index 0000000..3c37b2d --- /dev/null +++ b/mingling_picker/src/parselib/style.rs @@ -0,0 +1,258 @@ +use std::sync::OnceLock; +use std::sync::atomic::{AtomicBool, Ordering}; + +use crate::parselib::ParserStyleNamingCase::{Kebab, Pascal}; + +/// Defines the style of command-line argument parsing (prefixes, separators, etc.). +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct ParserStyle<'a> { + /// End-of-options marker (e.g., `--`) + pub end_of_options: &'a str, + + /// Prefix for long options (e.g., `--` or `/`) + pub long_prefix: &'a str, + + /// Prefix for short options (e.g., `-` or `/`) + pub short_prefix: &'a str, + + /// Prefix for combined short flags (e.g., `-abc`) + pub combine_prefix: &'a str, + + /// Separator between name and value (e.g., `=` or `:`) + pub value_separator: char, + + /// Whether option names are case-sensitive + pub case_sensitive: bool, + + /// Whether combining short flags is allowed (e.g., `-abc` for `-a -b -c`) + pub allow_combine: bool, + + /// Naming case + pub naming_case: ParserStyleNamingCase, +} + +impl<'a> ParserStyle<'a> { + /// Formats a flag (short or long) into a full command-line option string. + /// + /// This method takes any type that can be converted into a `FlagStr` and produces + /// a complete option string by prepending the appropriate prefix. + /// + /// # Examples + /// + /// ```ignore + /// use mingling_picker::parselib::{ParserStyle, FlagStr, UNIX_STYLE}; + /// let style = &UNIX_STYLE; + /// + /// assert_eq!(style.flag_string('v'), "-v"); + /// assert_eq!(style.flag_string("verbose"), "--verbose"); + /// ``` + /// + /// # Parameters + /// + /// * `flag` - A value that can be converted to `FlagStr`, either a `char` for short flags + /// or a `&str` for long flags. + /// + /// # Returns + /// + /// A `String` with the prefix and the flag name combined. + #[must_use] + #[inline(always)] + pub fn flag_string<F>(&self, flag: F) -> String + where + F: Into<FlagStr<'a>>, + { + match flag.into() { + FlagStr::Short(short) => format!("{}{}", self.short_prefix, short), + FlagStr::Long(long) => format!("{}{}", self.long_prefix, long), + } + } +} + +/// Represents a flag name for command-line argument parsing. +/// +/// This enum can hold either a short flag (a single character, e.g., `'v'` for `-v`) +/// or a long flag (a string, e.g., `"verbose"` for `--verbose`). +/// +/// # Examples +/// +/// ``` +/// use mingling_picker::parselib::FlagStr; +/// +/// let short: FlagStr = 'v'.into(); +/// let long: FlagStr = "verbose".into(); +/// ``` +pub enum FlagStr<'a> { + /// A short flag represented by a single character. + Short(char), + /// A long flag represented by a string slice. + Long(&'a str), +} + +impl<'a> From<char> for FlagStr<'a> { + /// Converts a single character into a `FlagStr::Short`. + fn from(c: char) -> Self { + FlagStr::Short(c) + } +} + +impl<'a> From<&'a str> for FlagStr<'a> { + /// Converts a string slice into a `FlagStr::Long`. + fn from(s: &'a str) -> Self { + FlagStr::Long(s) + } +} + +impl<'a> From<&'a String> for FlagStr<'a> { + /// Converts a reference to a `String` into a `FlagStr::Long`. + fn from(s: &'a String) -> Self { + FlagStr::Long(s.as_str()) + } +} + +/// Defines the naming convention for command-line option names. +/// +/// Each variant represents a different case format that can be applied +/// to option names (e.g., long option names) during parsing or generation. +/// +/// # Examples +/// +/// ``` +/// # use mingling_picker::IntoPicker; +/// use mingling_picker::parselib::ParserStyleNamingCase; +/// +/// let case = ParserStyleNamingCase::Kebab; +/// assert_eq!( +/// case.convert("brew_coffee".to_string()), +/// "brew-coffee".to_string() +/// ); +/// ``` +#[repr(u8)] +#[derive(Default, Clone, Copy, PartialEq, Eq)] +pub enum ParserStyleNamingCase { + /// snake_case format: words are separated by underscores, all lowercase. + /// + /// Example: `brew_coffee` + #[default] + Snake, + /// camelCase format: first word is lowercase, subsequent words are capitalized. + /// + /// Example: `brewCoffee` + Camel, + /// PascalCase format: every word starts with an uppercase letter. + /// + /// Example: `BrewCoffee` + Pascal, + /// kebab-case format: words are separated by hyphens, all lowercase. + /// + /// Example: `brew-coffee` + Kebab, + /// dot.case format: words are separated by dots, all lowercase. + /// + /// Example: `brew.coffee` + Dot, + /// Title Case format: words are separated by spaces, each word capitalized. + /// + /// Example: `Brew Coffee` + Title, + /// lower case format: words are separated by spaces, all lowercase. + /// + /// Example: `brew coffee` + Lower, + /// UPPER CASE format: words are separated by spaces, all uppercase. + /// + /// Example: `BREW COFFEE` + Upper, +} + +impl ParserStyleNamingCase { + /// Converts the input string `s` to the naming case represented by this variant. + /// + /// This method takes any type `S` that can be converted into a `String` and + /// produced from a `String`, applies the corresponding case transformation, + /// and returns the result. + /// + /// # Examples + /// + /// ``` + /// use mingling_picker::parselib::ParserStyleNamingCase; + /// + /// let camel = ParserStyleNamingCase::Camel; + /// assert_eq!(camel.convert("brew_coffee".to_string()), "brewCoffee"); + /// + /// let kebab = ParserStyleNamingCase::Kebab; + /// assert_eq!(kebab.convert("BrewCoffee".to_string()), "brew-coffee"); + /// ``` + pub fn convert<S>(&self, s: S) -> S + where + S: Into<String> + From<String>, + { + match self { + ParserStyleNamingCase::Camel => just_fmt::camel_case!(s.into()).into(), + ParserStyleNamingCase::Pascal => just_fmt::pascal_case!(s.into()).into(), + ParserStyleNamingCase::Kebab => just_fmt::kebab_case!(s.into()).into(), + ParserStyleNamingCase::Snake => just_fmt::snake_case!(s.into()).into(), + ParserStyleNamingCase::Dot => just_fmt::dot_case!(s.into()).into(), + ParserStyleNamingCase::Title => just_fmt::title_case!(s.into()).into(), + ParserStyleNamingCase::Lower => just_fmt::lower_case!(s.into()).into(), + ParserStyleNamingCase::Upper => just_fmt::upper_case!(s.into()).into(), + } + } +} + +/// Unix-like style (e.g., `--verbose`, `-v`, `--name=value`) +pub const UNIX_STYLE: ParserStyle = ParserStyle { + end_of_options: "--", + long_prefix: "--", + short_prefix: "-", + combine_prefix: "-", + value_separator: '=', + case_sensitive: true, + allow_combine: true, + naming_case: Kebab, +}; + +/// PowerShell style (e.g., `-Verbose`, `-Name:value`) +pub const POWERSHELL_STYLE: ParserStyle = ParserStyle { + end_of_options: "--", + long_prefix: "-", + short_prefix: "-", + combine_prefix: "-", + value_separator: ':', + case_sensitive: false, + allow_combine: false, + naming_case: Pascal, +}; + +/// Windows-style command-line (e.g., `/Verbose`, `/Name:value`) +pub const WINDOWS_STYLE: ParserStyle = ParserStyle { + end_of_options: "--", + long_prefix: "/", + short_prefix: "/", + combine_prefix: "/", + value_separator: ':', + case_sensitive: false, + allow_combine: false, + naming_case: Pascal, +}; + +static GLOBAL_STYLE: OnceLock<ParserStyle<'static>> = OnceLock::new(); +static GLOBAL_STYLE_SET: AtomicBool = AtomicBool::new(false); + +impl<'a> ParserStyle<'a> { + /// Sets the global parser style. + /// + /// This function can only be called once. Subsequent calls will have no effect. + /// The style is stored as a static reference; the provided style must be a static + /// constant (e.g., `&'static ParserStyle`). Use the built-in constants like + /// `UNIX_STYLE`, `POWERSHELL_STYLE`, or `WINDOWS_STYLE`. + pub fn set_global_style(style: &'static ParserStyle<'static>) { + if !GLOBAL_STYLE_SET.load(Ordering::Acquire) && GLOBAL_STYLE.set(*style).is_ok() { + GLOBAL_STYLE_SET.store(true, Ordering::Release); + } + } + + /// Returns the global parser style, falling back to `UNIX_STYLE` if not set. + pub fn global_style() -> &'static ParserStyle<'static> { + GLOBAL_STYLE.get().unwrap_or(&UNIX_STYLE) + } +} diff --git a/mingling_picker/src/parselib/utils.rs b/mingling_picker/src/parselib/utils.rs new file mode 100644 index 0000000..47c5b55 --- /dev/null +++ b/mingling_picker/src/parselib/utils.rs @@ -0,0 +1,276 @@ +use crate::{ + PickerArgInfo, + parselib::{MaskedArg, ParserStyle}, +}; + +/// Builds a list of possible flag strings for the given argument info +/// +/// This function generates formatted flag strings (e.g., `-h`, `--help`) from the short flag, +/// long flag, and any aliases defined in the argument info. The long flag and alias names +/// are converted according to the style's naming case convention before being formatted. +#[inline(always)] +pub fn build_possible_flags(style: &ParserStyle, arg_info: &PickerArgInfo) -> Vec<String> { + let mut possible_flags = vec![]; + + if let Some(short) = arg_info.short { + possible_flags.push(style.flag_string(short)); + } + + if let Some(long) = arg_info.long { + let converted = style.naming_case.convert(long.to_string()); + possible_flags.push(style.flag_string(&converted)); + } + + if let Some(aliases) = &arg_info.alias { + for alias in aliases { + let converted = style.naming_case.convert(alias.to_string()); + possible_flags.push(style.flag_string(&converted)); + } + } + + possible_flags +} + +/// Extract a single value from the raw strings tagged by [`SingleMatcher`](crate::parselib::SingleMatcher). +/// +/// Returns `None` if no value is available (empty slice), +/// the inline value after the style separator if present (eq mode), +/// or the value directly (positional or flag-following). +/// +/// This is the standard `pick` helper for all `Single`-type +/// [`Pickable`](crate::Pickable) implementations. +#[must_use] +pub fn seek_single<'a>(raw_strs: &'a [&'a str]) -> Option<&'a str> { + match raw_strs.len() { + 0 => None, + 1 => { + let s = raw_strs[0]; + let sep = ParserStyle::global_style().value_separator; + if let Some(pos) = s.rfind(sep) { + Some(&s[pos + 1..]) + } else { + Some(s) + } + } + _ => Some(raw_strs[1]), + } +} + +/// Seeks the index of the end-of-options marker (`--`) in the argument list. +/// +/// This function searches for the standard end-of-options separator (`--`) +/// in the given argument list, respecting the parser's style settings +/// (e.g., case sensitivity). The end-of-options marker indicates that all +/// subsequent arguments should be treated as positional arguments, not flags. +#[must_use] +pub fn seek_end_of_options(args: &[MaskedArg], style: &ParserStyle) -> Option<usize> { + args.iter() + .find(|arg| { + if style.case_sensitive { + arg.raw == style.end_of_options + } else { + arg.raw.eq_ignore_ascii_case(style.end_of_options) + } + }) + .map(|arg| arg.raw_idx) +} + +/// Seeks arguments in `args` that are exactly equal to the given `string`. +/// +/// Returns the indices of matching arguments. +#[must_use] +#[inline(always)] +pub fn seek_eq(args: &[MaskedArg], string: &str, case_sensitive: bool) -> Vec<usize> { + args.iter() + .filter(|arg| { + if case_sensitive { + arg.raw == string + } else { + arg.raw.eq_ignore_ascii_case(string) + } + }) + .map(|arg| arg.raw_idx) + .collect() +} + +/// Seeks arguments in `args` that contain the given `string` as a substring. +/// +/// Returns the indices of matching arguments. +#[must_use] +#[inline(always)] +pub fn seek_contains(args: &[MaskedArg], string: &str, case_sensitive: bool) -> Vec<usize> { + args.iter() + .filter(|arg| { + if case_sensitive { + arg.raw.contains(string) + } else { + arg.raw.to_lowercase().contains(&string.to_lowercase()) + } + }) + .map(|arg| arg.raw_idx) + .collect() +} + +/// Seeks arguments in `args` that start with the given `string`. +/// +/// Returns the indices of matching arguments. +#[must_use] +#[inline(always)] +pub fn seek_start_with(args: &[MaskedArg], string: &str, case_sensitive: bool) -> Vec<usize> { + args.iter() + .filter(|arg| { + if case_sensitive { + arg.raw.starts_with(string) + } else { + arg.raw.to_lowercase().starts_with(&string.to_lowercase()) + } + }) + .map(|arg| arg.raw_idx) + .collect() +} + +/// Seeks arguments in `args` that end with the given `string`. +/// +/// Returns the indices of matching arguments. +#[must_use] +#[inline(always)] +pub fn seek_end_with(args: &[MaskedArg], string: &str, case_sensitive: bool) -> Vec<usize> { + args.iter() + .filter(|arg| { + if case_sensitive { + arg.raw.ends_with(string) + } else { + arg.raw.to_lowercase().ends_with(&string.to_lowercase()) + } + }) + .map(|arg| arg.raw_idx) + .collect() +} + +/// Seeks arguments in `args` that are exactly equal to any of the given `strings`. +/// +/// Returns the indices of matching arguments. +#[must_use] +#[inline(always)] +pub fn multi_seek_eq(args: &[MaskedArg], strings: &[&str], case_sensitive: bool) -> Vec<usize> { + args.iter() + .filter(|arg| { + if case_sensitive { + strings.contains(&arg.raw) + } else { + strings.iter().any(|s| arg.raw.eq_ignore_ascii_case(s)) + } + }) + .map(|arg| arg.raw_idx) + .collect() +} + +/// Seeks arguments in `args` that contain any of the given `strings` as a substring. +/// +/// Returns the indices of matching arguments. +#[must_use] +#[inline(always)] +pub fn multi_seek_contains( + args: &[MaskedArg], + strings: &[&str], + case_sensitive: bool, +) -> Vec<usize> { + args.iter() + .filter(|arg| { + if case_sensitive { + strings.iter().any(|s| arg.raw.contains(s)) + } else { + let lower_raw = arg.raw.to_lowercase(); + strings + .iter() + .any(|s| lower_raw.contains(&s.to_lowercase())) + } + }) + .map(|arg| arg.raw_idx) + .collect() +} + +/// Seeks arguments in `args` that start with any of the given `strings`. +/// +/// Returns the indices of matching arguments. +#[must_use] +#[inline(always)] +pub fn multi_seek_start_with( + args: &[MaskedArg], + strings: &[&str], + case_sensitive: bool, +) -> Vec<usize> { + args.iter() + .filter(|arg| { + if case_sensitive { + strings.iter().any(|s| arg.raw.starts_with(s)) + } else { + let lower_raw = arg.raw.to_lowercase(); + strings + .iter() + .any(|s| lower_raw.starts_with(&s.to_lowercase())) + } + }) + .map(|arg| arg.raw_idx) + .collect() +} + +/// Seeks arguments in `args` that end with any of the given `strings`. +/// +/// Returns the indices of matching arguments. +#[must_use] +#[inline(always)] +pub fn multi_seek_end_with( + args: &[MaskedArg], + strings: &[&str], + case_sensitive: bool, +) -> Vec<usize> { + args.iter() + .filter(|arg| { + if case_sensitive { + strings.iter().any(|s| arg.raw.ends_with(s)) + } else { + let lower_raw = arg.raw.to_lowercase(); + strings + .iter() + .any(|s| lower_raw.ends_with(&s.to_lowercase())) + } + }) + .map(|arg| arg.raw_idx) + .collect() +} + +/// Converts a `&Vec<String>` into a `Vec<&str>` by borrowing each string's slice. +/// +/// This is useful for converting owned `String` vectors into borrowed `&str` slices +/// for functions that take `&[&str]` or similar parameters. +#[must_use] +#[inline(always)] +#[doc(hidden)] +pub fn vec_string_to_vec_str(input: &[String]) -> Vec<&str> { + input.iter().map(|s| s.as_str()).collect() +} + +/// Converts a `&Vec<String>` into a `Vec<&str>` by borrowing each string's slice. +/// +/// This is useful for converting owned `String` vectors into borrowed `&str` slices +/// for functions that take `&[&str]` or similar parameters. +#[macro_export] +#[doc(hidden)] +macro_rules! vec_string_slice { + ($v:expr) => { + $v.iter() + .map(|s| s.as_str()) + .collect::<Vec<&str>>() + .as_slice() + }; +} + +/// Gets the first element from a vector of seek results, if any. +/// +/// Returns `Some(index)` if the vector is non-empty, otherwise `None`. +#[must_use] +#[inline(always)] +pub fn get_seeked_first(seeked: Vec<usize>) -> Option<usize> { + seeked.into_iter().next() +} diff --git a/mingling_picker/src/pickable.rs b/mingling_picker/src/pickable.rs new file mode 100644 index 0000000..758ae9a --- /dev/null +++ b/mingling_picker/src/pickable.rs @@ -0,0 +1,91 @@ +use crate::{PickerArg, PickerArgAttr, PickerArgInfo, PickerArgResult, PickerArgs}; + +mod single_pickable; +pub use single_pickable::*; + +mod multi_pickable; +pub use multi_pickable::*; + +/// `Pickable` trait defines how to parse a type instance from command-line arguments. +/// +/// This trait is the core abstraction of the `Picker` argument parsing system, dividing the +/// parsing process into two phases: +/// +/// 1. **Tag phase ([`Pickable::tag`])**: Determines which argument positions the `Pickable` needs to handle. +/// 2. **Pick phase ([`Pickable::pick`])**: Converts the raw strings at the tagged positions into the actual type. +/// +/// Types implementing this trait must also implement [`Default`], so that a default value +/// can be used as a fallback when parsing fails. +/// +/// # Type Parameters +/// +/// * `'a` - Lifetime parameter, used to associate references in [`PickerArg`]. +pub trait Pickable<'a> +where + Self: Sized, +{ + /// Returns the parse-order attribute of this flag. + /// + /// This attribute is used to inform the parser about the parse order + /// between different `Pickable` types. + /// See [`PickerArgAttr`] for specific ordering definitions. + /// + /// # Parameters + /// + /// * `flag` - The current flag instance, which contains a reference to `Self`. + /// + /// # Returns + /// + /// Returns a [`PickerArgAttr`] describing the parse-order attribute of this flag. + fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr; + + /// Tag phase: Determines which argument positions the `Pickable` needs to handle. + /// + /// This function receives a [`TagPhaseContext`] containing argument context information. + /// During this phase, the parser invokes each `Pickable` and collects the position indices + /// they return, in order to determine which arguments to parse later. + /// + /// # Parameters + /// + /// * `ctx` - The tag phase context, containing argument information, all parameters of the + /// current Picker, and an availability mask. + /// + /// # Returns + /// + /// Returns a `Vec<usize>` representing the indices of the arguments in the argument list + /// that this `Pickable` needs to handle. + fn tag(ctx: TagPhaseContext) -> Vec<usize>; + + /// Pick phase: Converts the raw string arguments tagged during the `tag` phase into + /// the actual expected type. + /// + /// This function receives a slice of the raw strings that were tagged in the `tag` step + /// and converts them into an instance of `Self`. + /// + /// # Parameters + /// + /// * `raw_strs` - A slice of strings containing the raw argument values to parse. + /// + /// # Returns + /// + /// Returns [`PickerArgResult<Self>`], i.e., the `Self` instance on success, or an appropriate + /// error message on failure. + fn pick(raw_strs: &[&str]) -> PickerArgResult<Self>; +} + +/// Tag phase context, providing the necessary argument and state information for +/// [`Pickable::tag`]. +pub struct TagPhaseContext<'a> { + /// Argument information describing the structure and metadata of the argument + /// to be parsed. + pub arg_info: &'a PickerArgInfo<'a>, + + /// A read-only list of all arguments in the current [`Picker`](crate::Picker). + pub args: &'a PickerArgs<'a>, + + /// Mask indicating which argument positions have already been claimed. + /// + /// For example, if the mask is `[0, 0, 1, 0]`, then the argument at index `2` + /// has already been tagged by another `Pickable`. + pub mask: &'a [u8], +} diff --git a/mingling_picker/src/pickable/multi_pickable.rs b/mingling_picker/src/pickable/multi_pickable.rs new file mode 100644 index 0000000..84a8068 --- /dev/null +++ b/mingling_picker/src/pickable/multi_pickable.rs @@ -0,0 +1,76 @@ +use crate::{ + Pickable, PickerArg, PickerArgAttr, PickerArgResult, SinglePickable, TagPhaseContext, + matcher_needed::Matcher, + parselib::{MultiArgMatcher, ParserStyle}, +}; + +/// Boundary check for multi-value positional parameters. +pub trait BoundaryCheck { + fn check_boundary(raw: &str) -> bool; +} + +/// Trait for multi-value parameters. +pub trait MultiPickableWithBoundary: Sized { + type Checker: BoundaryCheck; + fn pick_multi(raw: Vec<String>) -> PickerArgResult<Self>; +} + +/// Marker: unit type that always accepts — no boundary. +pub struct NoBoundary; + +impl BoundaryCheck for NoBoundary { + #[inline(always)] + fn check_boundary(_raw: &str) -> bool { + false + } +} + +/// `Vec<T>` is greedy — it takes everything with `NoBoundary`. +impl<T: SinglePickable> MultiPickableWithBoundary for Vec<T> { + type Checker = NoBoundary; + + fn pick_multi(raw: Vec<String>) -> PickerArgResult<Self> { + let mut result = Vec::with_capacity(raw.len()); + for s in &raw { + match T::pick_single(Some(s)) { + PickerArgResult::Parsed(v) => result.push(v), + PickerArgResult::NotFound => return PickerArgResult::NotFound, + PickerArgResult::Unparsed => {} + } + } + PickerArgResult::Parsed(result) + } +} + +/// If the first raw string looks like a named flag (starts with the +/// style's long or short prefix), strip it — it's the flag, not a value. +fn strip_flag<'a>(raw_strs: &'a [&'a str]) -> &'a [&'a str] { + if let Some(first) = raw_strs.first() { + let style = ParserStyle::global_style(); + if first.starts_with(style.long_prefix) || first.starts_with(style.short_prefix) { + return &raw_strs[1..]; + } + } + raw_strs +} + +// Pickable impl for Vec<T> + +impl<'a, T> Pickable<'a> for Vec<T> +where + T: SinglePickable, +{ + fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr { + PickerArgAttr::positional_or_multi(flag) + } + + fn tag(ctx: TagPhaseContext) -> Vec<usize> { + MultiArgMatcher::match_all(ctx.into()) + } + + fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> { + let strs = strip_flag(raw_strs); + let owned: Vec<String> = strs.iter().map(|&s| s.to_string()).collect(); + <Vec<T> as MultiPickableWithBoundary>::pick_multi(owned) + } +} diff --git a/mingling_picker/src/pickable/single_pickable.rs b/mingling_picker/src/pickable/single_pickable.rs new file mode 100644 index 0000000..8a5b3e6 --- /dev/null +++ b/mingling_picker/src/pickable/single_pickable.rs @@ -0,0 +1,69 @@ +use crate::{Pickable, PickerArg, PickerArgAttr, PickerArgResult, TagPhaseContext}; + +/// `SinglePickable` trait defines how to parse a type from a single command-line argument. +/// +/// This trait provides a simplified interface for types that consume exactly one argument value. +/// It is automatically implemented by the blanket `impl` of [`Pickable`], so types implementing +/// `SinglePickable` will work with the full `Pickable` argument parsing system. +/// +/// Additionally, `Option<S>` where `S: SinglePickable` also implements [`Pickable`], allowing +/// optional arguments to be parsed naturally. +/// +/// # Type Parameters +/// +/// * `Self` - The type to be parsed from a single argument string. +pub trait SinglePickable +where + Self: Sized, +{ + /// Parse a single optional string value into an instance of `Self`. + /// + /// # Parameters + /// + /// * `str` - An `Option<&str>` representing the raw argument value. If `None`, + /// it indicates that no argument value was provided (e.g., for flag-like arguments). + /// + /// # Returns + /// + /// Returns [`PickerArgResult<Self>`], i.e., the parsed `Self` instance on success, + /// or an appropriate error message on failure. + fn pick_single(str: Option<&str>) -> PickerArgResult<Self>; +} + +impl<'a, S> Pickable<'a> for S +where + S: SinglePickable, +{ + fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr { + PickerArgAttr::positional_or_single(flag) + } + + fn tag(ctx: TagPhaseContext) -> Vec<usize> { + crate::parselib::SingleMatcher::tag(ctx) + } + + fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> { + Self::pick_single(crate::parselib::seek_single(raw_strs)) + } +} + +impl<'a, S> Pickable<'a> for Option<S> +where + S: SinglePickable, +{ + fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr { + PickerArgAttr::positional_or_single(flag) + } + + fn tag(ctx: TagPhaseContext) -> Vec<usize> { + crate::parselib::SingleMatcher::tag(ctx) + } + + fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> { + match S::pick(raw_strs) { + PickerArgResult::Unparsed => PickerArgResult::Unparsed, + PickerArgResult::Parsed(r) => PickerArgResult::Parsed(Some(r)), + PickerArgResult::NotFound => PickerArgResult::Parsed(None), + } + } +} diff --git a/mingling_picker/src/picker.rs b/mingling_picker/src/picker.rs new file mode 100644 index 0000000..69b1671 --- /dev/null +++ b/mingling_picker/src/picker.rs @@ -0,0 +1,354 @@ +use std::{marker::PhantomData, ops::Index}; + +mod parse; + +mod patterns; +pub use patterns::*; + +mod result; +pub use result::*; + +use crate::{Pickable, PickerArg, PickerArgResult}; + +#[doc = include_str!("../README.md")] +pub struct Picker<'a, Route = ()> { + route_phantom: PhantomData<Route>, + + /// Internal arguments of Picker + args: PickerArgs<'a>, +} + +impl<'a> Picker<'a> { + /// Creates a new `Picker` from the command-line arguments (excluding the program name). + /// + /// This is equivalent to calling `std::env::args().skip(1)`, which + /// collects all arguments passed to the program except the first one + /// (the executable path). + pub fn from_args() -> Picker<'a, ()> { + Self::from_args_skip(1) + } + + /// Creates a new `Picker` from the command-line arguments, skipping the + /// first `skip` entries. + /// + /// This method is useful when you want more control over which arguments + /// are included. For example, pass `skip = 2` to skip both the program + /// name and the first argument. + pub fn from_args_skip(skip: usize) -> Picker<'a, ()> { + let args = std::env::args().skip(skip).collect::<Vec<String>>(); + Picker { + route_phantom: PhantomData, + args: PickerArgs::Owned(args), + } + } + + /// Changes the route (phantom type parameter) of the `Picker`. + /// + /// This method allows converting a `Picker` from one route type to another, + /// while preserving the same underlying arguments. The route type is typically + /// used to distinguish different parsing contexts or to carry compile-time + /// state information through the picking chain. + pub fn with_route<NewRoute>(self) -> Picker<'a, NewRoute> + where + Self: Sized, + { + Picker { + route_phantom: PhantomData, + args: self.args, + } + } +} + +/// Internal arguments of Picker +/// +/// - `Slice` - borrowed slice of string slices +/// - `Vec` - owned vector of borrowed string slices +/// - `Owned` - owned vector of owned strings +pub enum PickerArgs<'a> { + /// Borrowed slice of string slices + Slice(&'a [&'a str]), + /// Owned vector of borrowed string slices + Vec(Vec<&'a str>), + /// Owned vector of owned strings + Owned(Vec<String>), +} + +impl<'a> Default for PickerArgs<'a> { + fn default() -> Self { + Self::Vec(vec![]) + } +} + +impl<'a> PickerArgs<'a> { + /// Returns the number of arguments. + pub fn len(&self) -> usize { + match self { + PickerArgs::Slice(items) => items.len(), + PickerArgs::Vec(items) => items.len(), + PickerArgs::Owned(items) => items.len(), + } + } + + /// Returns `true` if there are no arguments. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns an iterator over the arguments, yielding `&str` values. + pub fn iter(&'a self) -> PickerIter<'a> { + match self { + PickerArgs::Slice(items) => PickerIter::Slice(items.iter()), + PickerArgs::Vec(items) => PickerIter::Vec(items.iter()), + PickerArgs::Owned(items) => PickerIter::Owned(items.iter()), + } + } + + /// Returns a reference to the argument at `index`, if it exists. + pub fn get(&self, index: usize) -> Option<&str> { + match self { + PickerArgs::Slice(items) => items.get(index).copied(), + PickerArgs::Vec(items) => items.get(index).copied(), + PickerArgs::Owned(items) => items.get(index).map(|s| s.as_str()), + } + } +} + +impl<'a> Index<usize> for PickerArgs<'a> { + type Output = str; + + fn index(&self, index: usize) -> &Self::Output { + match self { + PickerArgs::Slice(items) => items[index], + PickerArgs::Vec(items) => items[index], + PickerArgs::Owned(items) => &items[index], + } + } +} + +impl<'a> IntoIterator for &'a PickerArgs<'a> { + type Item = &'a str; + type IntoIter = PickerIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + match self { + PickerArgs::Slice(items) => PickerIter::Slice(items.iter()), + PickerArgs::Vec(items) => PickerIter::Vec(items.iter()), + PickerArgs::Owned(items) => PickerIter::Owned(items.iter()), + } + } +} + +impl<'a, Route> From<&'a [&'a str]> for Picker<'a, Route> { + fn from(value: &'a [&'a str]) -> Self { + Picker { + route_phantom: PhantomData, + args: PickerArgs::Slice(value), + } + } +} + +impl<'a, Route> From<Vec<&'a str>> for Picker<'a, Route> { + fn from(value: Vec<&'a str>) -> Self { + Picker { + route_phantom: PhantomData, + args: PickerArgs::Vec(value), + } + } +} + +impl<'a, Route> From<Vec<String>> for Picker<'a, Route> { + fn from(value: Vec<String>) -> Self { + Picker { + route_phantom: PhantomData, + args: PickerArgs::Owned(value), + } + } +} + +impl<'a, Route> Picker<'a, Route> { + /// Returns a reference to the internal `PickerArgs`. + pub fn args(&self) -> &PickerArgs<'a> { + &self.args + } + + /// Returns a mutable reference to the internal `PickerArgs`. + pub fn args_mut(&mut self) -> &mut PickerArgs<'a> { + &mut self.args + } + + /// Consumes `self` and returns the internal `PickerArgs`. + pub fn into_args(self) -> PickerArgs<'a> { + self.args + } + + /// Returns the number of arguments. + pub fn len(&self) -> usize { + self.args.len() + } + + /// Returns `true` if there are no arguments. + pub fn is_empty(&self) -> bool { + self.args.is_empty() + } + + /// Returns an iterator over the arguments, yielding `&str` values. + pub fn iter(&'a self) -> PickerIter<'a> { + self.args.iter() + } +} + +impl<'a, Route> Index<usize> for Picker<'a, Route> { + type Output = str; + + fn index(&self, index: usize) -> &Self::Output { + &self.args[index] + } +} + +impl<'a, Route> Index<usize> for &Picker<'a, Route> { + type Output = str; + + fn index(&self, index: usize) -> &Self::Output { + &self.args[index] + } +} + +impl<'a, Route> IntoIterator for &'a Picker<'a, Route> { + type Item = &'a str; + type IntoIter = PickerIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + self.args.iter() + } +} + +/// Iterator for `Picker` (and `PickerArgs`), yielding `&'a str` values. +pub enum PickerIter<'a> { + /// Iterates over a borrowed slice (`&[&str]`) + Slice(std::slice::Iter<'a, &'a str>), + /// Iterates over an owned vector of borrowed string slices (`Vec<&str>`) + Vec(std::slice::Iter<'a, &'a str>), + /// Iterates over an owned vector of owned strings (`Vec<String>`) + Owned(std::slice::Iter<'a, String>), +} + +impl<'a> Iterator for PickerIter<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option<Self::Item> { + match self { + PickerIter::Slice(iter) => iter.next().copied(), + PickerIter::Vec(iter) => iter.next().copied(), + PickerIter::Owned(iter) => iter.next().map(|s| s.as_str()), + } + } + + fn size_hint(&self) -> (usize, Option<usize>) { + match self { + PickerIter::Slice(iter) => iter.size_hint(), + PickerIter::Vec(iter) => iter.size_hint(), + PickerIter::Owned(iter) => iter.size_hint(), + } + } +} + +impl<'a> ExactSizeIterator for PickerIter<'a> {} + +/// Trait for converting types into a `Picker` +/// +/// Implemented for: +/// - `&[&str]` (borrowed slice) +/// - `&[String]` (borrowed slice of owned strings) +/// - `Vec<&str>` (owned vector of borrowed strings) +/// - `Vec<String>` (owned vector of owned strings) +pub trait IntoPicker<'a> { + /// Converts the value into a `Picker` + /// + /// # Examples + /// + /// ``` + /// use mingling_picker::{IntoPicker, Picker}; + /// + /// let args: Picker = (&["hello", "world"][..]).to_picker(); + /// assert_eq!(args.len(), 2); + /// + /// let args: Picker = vec!["foo", "bar"].to_picker(); + /// assert_eq!(args.len(), 2); + /// + /// let args: Picker = vec!["a".to_string(), "b".to_string()].to_picker(); + /// assert_eq!(args.len(), 2); + /// ``` + fn to_picker(self) -> Picker<'a, ()>; + + /// Creates a `PickerPattern1` from the given arg for the `pick` method. + /// + /// This method converts the value into a `Picker` and starts a parameter + /// picking chain with one arg. The result is initially `Unparsed`. + fn pick<N>(self, arg: impl Into<&'a PickerArg<'a, N>>) -> PickerPattern1<'a, N, ()> + where + Self: Sized, + N: Pickable<'a> + Default + Sized, + { + PickerPattern1 { + args: self.to_picker().args, + arg_1: arg.into(), + result_1: PickerArgResult::Unparsed, + default_1: None, + route_1: None, + post_1: None, + error_route: None, + } + } + + /// Converts the value into a `Picker` with a specified route type. + /// + /// This method allows changing the route (phantom type parameter) of the picker. + /// The route type is typically used to distinguish different parsing contexts or + /// to carry compile-time state information through the picking chain. + fn with_route<NewRoute>(self) -> Picker<'a, NewRoute> + where + Self: Sized, + { + Picker { + route_phantom: PhantomData, + args: self.to_picker().args, + } + } +} + +impl<'a> IntoPicker<'a> for &'a [&'a str] { + fn to_picker(self) -> Picker<'a, ()> { + Picker { + route_phantom: PhantomData, + args: PickerArgs::Slice(self), + } + } +} + +impl<'a> IntoPicker<'a> for &'a [String] { + fn to_picker(self) -> Picker<'a, ()> { + let vec: Vec<&str> = self.iter().map(|s| s.as_str()).collect(); + Picker { + route_phantom: PhantomData, + args: PickerArgs::Vec(vec), + } + } +} + +impl<'a> IntoPicker<'a> for Vec<&'a str> { + fn to_picker(self) -> Picker<'a, ()> { + Picker { + route_phantom: PhantomData, + args: PickerArgs::Vec(self), + } + } +} + +impl<'a> IntoPicker<'a> for Vec<String> { + fn to_picker(self) -> Picker<'a, ()> { + Picker { + route_phantom: PhantomData, + args: PickerArgs::Owned(self), + } + } +} diff --git a/mingling_picker/src/picker/parse.rs b/mingling_picker/src/picker/parse.rs new file mode 100644 index 0000000..8f7d514 --- /dev/null +++ b/mingling_picker/src/picker/parse.rs @@ -0,0 +1,177 @@ +// -------------------------------------------------------------------------------------------- +// I have to say, the code generated by this `internal_repeat!` macro is really UGLY. +// +// But I have to admit, this is a **trade-off**. To achieve the syntax of `pick().pick().pick()` +// while ensuring type safety, this is the best approach I could think of. +// +// P.S. If there's a better way, please let me know. Thanks! +// -------------------------------------------------------------------------------------------- +// +// Then, I must disable `clippy::type_complexity` — this guy is way too noisy. +#![allow(clippy::type_complexity)] + +use crate::{Pickable, PickerArgAttr, PickerArgInfo, PickerArgResult, PickerArgs, TagPhaseContext}; +use mingling_picker_macros::internal_repeat; + +internal_repeat!(1..=32 => { + use crate::PickerPattern$; + use crate::PickerResult$; +}); + +internal_repeat!(1..=32 => { + impl<'a, (T$,+), Route> PickerPattern$<'a, (T$,+), Route> + where (T$: Pickable<'a>,+) { + /// Unwraps the result, panicking if a route was selected. + /// + /// # Panics + /// + /// Panics if a route was selected. + pub fn unwrap(self) -> ((T$,+)) { + let p = self.parse(); + ((p.v$.unwrap(),+)) + } + + /// Returns the individual option values without checking the route. + pub fn unpack(self) -> ((Option<T$>,+)) { + let p = self.parse(); + ((p.v$,+)) + } + + /// Converts to a `Result`, returning `Err(route)` if a route was selected, + /// or `Ok(values)` otherwise. + pub fn to_result(self) -> Result<((T$,+)), Route> { + let p = self.parse(); + if let Some(r) = p.route { + return Err(r); + } + Ok(p.unwrap()) + } + + /// Converts to an `Option`, returning `None` if a route was selected, + /// or `Some(values)` otherwise. + pub fn to_option(self) -> Option<((T$,+))> { + let p = self.parse(); + if p.route.is_some() { + return None; + } + Some(p.unwrap()) + } + } +}); + +internal_repeat!(1..=32 => { + impl<'a, (T$,+), Route> PickerPattern$<'a, (T$,+), Route> + where (T$: Pickable<'a>,+) + { + pub fn parse(mut self) -> PickerResult$<(T$,+), Route> { + // ArgInfos + let arg_infos: [PickerArgInfo; $] = [ + ( + PickerArgInfo::from(self.arg_$), + +) + ]; + + let mut bundle: [ + ( + // Arg Attr + PickerArgAttr, + + // Tag Func + Box<dyn FnOnce(&PickerArgs<'a>, &[u8]) -> Vec<usize>>, + + // Pick Func + Box<dyn FnOnce(&[&str], &mut Option<Route>)>, + + // Index + usize + ) + ; $] = [ + ( + ( + // Arg Attr + T$::get_attr(self.arg_$), + + // Tag Func + Box::new(|args, mask| { + let ctx = TagPhaseContext { + arg_info: &arg_infos[$-], + args, + mask + }; + T$::tag(ctx) + }), + + // Pick Func + Box::new(|args, error_route| { + self.result_$ = match T$::pick(args) { + PickerArgResult::Parsed(mut value) => { + // Postprocess + if let Some(post) = self.post_$ { + value = post(value); + } + PickerArgResult::Parsed(value) + }, + other => { + if let Some(get_default) = self.default_$ { + let mut value = get_default(); + + // Postprocess + if let Some(post) = self.post_$ { + value = post(value); + } + + PickerArgResult::Parsed(value) + } else { + if error_route.is_none() { + if let Some(get_route) = self.route_$ { + *error_route = Some(get_route()); + } + } + other + } + }, + + } + }), + + // Index + $ + ), + +) + ]; + + // Sort by Bundle Ord (descending) + bundle.sort_by(|a, b| b.0.cmp(&a.0)); + + // Mask — size = number of args (not args), so use args length + let mut mask: Vec<u8> = vec![0u8; self.args.len()]; + + // Parsing + for (_, tag_func, pick_func, _idx) in bundle { + + // Tag phase + let tagged = tag_func(&self.args, mask.as_slice()); + let mut args_to_pick: Vec<&str> = vec![]; + + for i in tagged { + mask[i] = 1; + + // Update args to pick + args_to_pick.push(self.args.get(i).unwrap_or_default()); + } + + // Pick phase + pick_func(args_to_pick.as_slice(), &mut self.error_route); + } + + // Combine Result + let result: PickerResult$<(T$,+), Route> = PickerResult$ { + route: self.error_route, + ( + v$: self.result_$.to_option(), + +) + }; + result + } + } +}); diff --git a/mingling_picker/src/picker/patterns.rs b/mingling_picker/src/picker/patterns.rs new file mode 100644 index 0000000..3c6e73f --- /dev/null +++ b/mingling_picker/src/picker/patterns.rs @@ -0,0 +1,199 @@ +use mingling_picker_macros::internal_repeat; + +use crate::{Pickable, Picker, PickerArg, PickerArgResult, PickerArgs}; + +internal_repeat!(1..=32 => { + #[doc(hidden)] + pub struct PickerPattern$<'a, (T$,+), Route> + where (T$: Pickable<'a>,+) + { + pub args: PickerArgs<'a>, + pub error_route: Option<Route>, + ( + pub arg_$: &'a PickerArg<'a, T$>, + pub result_$: PickerArgResult<T$>, + pub default_$: Option<Box<dyn FnOnce() -> T$>>, + pub route_$: Option<Box<dyn FnOnce() -> Route>>, + pub post_$: Option<Box<dyn FnOnce(T$) -> T$>>, + +) + } +}); + +internal_repeat!(1..=32 => { + impl<'a, (T$,+), Route> PickerPattern$<'a, (T$,+), Route> + where (T$: Pickable<'a>,+) + { + /// Sets a default value provider for this arg. + /// + /// If the arg is not provided by the user at runtime, the given closure will be + /// called to produce a default value. The closure is expected to return `T$`. + /// + /// # Example + /// + /// ```ignore + /// let pattern = picker + /// .pick(&my_arg) + /// .or(|| 42); + /// ``` + #[allow(clippy::type_complexity)] + pub fn or<F>(mut self, func: F) -> Self + where + F: FnMut() -> T$, + F: 'static, + { + self.default_$ = Some(Box::new(func)); + self + } + + /// Uses the default value for this arg's type if the arg is not provided. + /// + /// If the arg is not provided by the user at runtime, the default value for `T$` + /// (as defined by the `Default` trait) will be used. + /// + /// # Example + /// + /// ```ignore + /// let pattern = picker + /// .pick(&my_arg) + /// .or_default(); + /// ``` + #[allow(clippy::type_complexity)] + pub fn or_default(mut self) -> Self + where + T$: Default, + { + self.default_$ = Some(Box::new(|| T$::default())); + self + } + + /// Sets a route for when the arg is not provided. + /// + /// If the arg is not provided by the user at runtime, the given closure will be + /// called to produce a route value that will be returned early. + /// + /// # Example + /// + /// ```ignore + /// let pattern = picker + /// .pick(&my_arg) + /// .or_route(|| Redirect::home()); + /// ``` + pub fn or_route<F>(mut self, func: F) -> Self + where + F: FnMut() -> Route, + F: 'static, + { + self.route_$ = Some(Box::new(func)); + self + } + + + /// Resets the route for this picker pattern, allowing a different route type. + /// + /// This method converts the current `PickerPattern` into a new one with a different + /// route type `NewRoute`. All existing arg configurations, defaults, and post- + /// processing functions are preserved, but the `error_route` and individual + /// `route_$` fields are cleared (set to `None`). + /// + /// This is useful when you want to change the error/redirect route type mid-chain, + /// for example when composing patterns from different contexts that use different + /// route enums. + #[allow(clippy::type_complexity)] + pub fn with_route<NewRoute>(self) -> PickerPattern$<'a, (T$,+), NewRoute> { + PickerPattern$ { + args: self.args, + error_route: None, + ( + arg_$: self.arg_$, + result_$: self.result_$, + default_$: self.default_$, + route_$: None, + post_$: self.post_$, + +) + } + } + + /// Attaches a post-processing function to this arg. + /// + /// After the arg's value is parsed (or defaulted), the given closure will be + /// invoked with the parsed value and its return value will be used as the final + /// result. This allows transforming or validating the parsed value. + /// + /// # Example + /// + /// ```ignore + /// let pattern = picker + /// .pick(&my_arg) + /// .post(|val| val * 2); + /// ``` + #[allow(clippy::type_complexity)] + pub fn post<F>(mut self, func: F) -> Self + where + F: FnMut(T$) -> T$, + F: 'static, + { + self.post_$ = Some(Box::new(func)); + self + } + } +}); + +internal_repeat!(1..32 => { + impl<'a, (T$,+), Route> PickerPattern$<'a, (T$,+), Route> + where (T$: Pickable<'a>,+) + { + #[allow(clippy::type_complexity)] + /// Adds a new arg to the picking chain, returning a new `PickerPattern` with one more type parameter. + /// + /// This method extends the current picking pattern by appending an additional arg. + /// The previous args and their results are preserved as part of the new pattern. + /// The new arg's result is initially `Unparsed`. + pub fn pick<N>(self, arg: impl Into<&'a PickerArg<'a, N>>) -> PickerPattern$+<'a, (T$,+), N, Route> + where + N: Pickable<'a>, + { + PickerPattern$+ { + // Args + args: self.args, + error_route: self.error_route, + + // Current + arg_$+: arg.into(), + result_$+: PickerArgResult::Unparsed, + default_$+: None, + route_$+: None, + post_$+: None, + + // Prev + ( + arg_$: self.arg_$, + result_$: self.result_$, + default_$: self.default_$, + route_$: self.route_$, + post_$: self.post_$, + +) + } + } + } +}); + +impl<'a, Route> Picker<'a, Route> { + /// Creates a `PickerPattern1` from the given arg to start a picking chain. + /// + /// This method initiates a parameter picking chain with one arg. + /// The result is initially `Unparsed`. + pub fn pick<N>(self, arg: impl Into<&'a PickerArg<'a, N>>) -> PickerPattern1<'a, N, Route> + where + N: Pickable<'a>, + { + PickerPattern1 { + args: self.args, + error_route: None::<Route>, + arg_1: arg.into(), + result_1: PickerArgResult::Unparsed, + route_1: None, + default_1: None, + post_1: None, + } + } +} diff --git a/mingling_picker/src/picker/result.rs b/mingling_picker/src/picker/result.rs new file mode 100644 index 0000000..9cd78ae --- /dev/null +++ b/mingling_picker/src/picker/result.rs @@ -0,0 +1,56 @@ +#![allow(clippy::type_complexity)] // Aha, Type Gymnastics! + +use mingling_picker_macros::internal_repeat; + +internal_repeat!(1..=32 => { + #[doc(hidden)] + pub struct PickerResult$<(T$,+), Route> { + /// The route selected by the picker, if any. + /// If this is `Some`, the picker chose to follow a route instead of selecting values, + /// and all value fields (`v1`, `v2`, ...) will be `None`. + /// + /// Note: "route" here refers to an alternative path/choice, not a network route. + pub route: Option<Route>, + + ( + #[doc = concat!("The optional value for the ", $, "th type parameter.")] + pub v$: Option<T$>, + +) + } +}); + +internal_repeat!(1..=32 => { + impl<(T$,+), Route> PickerResult$<(T$,+), Route> { + /// Unwraps the result, panicking if a route was selected. + /// + /// # Panics + /// + /// Panics if `self.route` is `Some(...)`. + pub fn unwrap(self) -> ((T$,+)) { + ((self.v$.unwrap(),+)) + } + + /// Returns the individual option values without checking the route. + pub fn unpack(self) -> ((Option<T$>,+)) { + ((self.v$,+)) + } + + /// Converts to a `Result`, returning `Err(route)` if a route was selected, + /// or `Ok(values)` otherwise. + pub fn to_result(self) -> Result<((T$,+)), Route> { + if let Some(r) = self.route { + return Err(r); + } + Ok(self.unwrap()) + } + + /// Converts to an `Option`, returning `None` if a route was selected, + /// or `Some(values)` otherwise. + pub fn to_option(self) -> Option<((T$,+))> { + if let Some(_) = self.route { + return None; + } + Some(self.unwrap()) + } + } +}); diff --git a/mingling_picker/src/value.rs b/mingling_picker/src/value.rs new file mode 100644 index 0000000..995aa00 --- /dev/null +++ b/mingling_picker/src/value.rs @@ -0,0 +1,5 @@ +mod flag; +pub use flag::*; + +mod vec_until; +pub use vec_until::*; diff --git a/mingling_picker/src/value/flag.rs b/mingling_picker/src/value/flag.rs new file mode 100644 index 0000000..ee0d6ee --- /dev/null +++ b/mingling_picker/src/value/flag.rs @@ -0,0 +1,145 @@ +use std::{ + fmt::{Debug, Display}, + ops::{Deref, Not}, +}; + +/// Parsed result of a boolean-style command-line flag. +/// +/// `Flag` is a **value type** that can be declared in [`PickerArg`]. +/// When the user passes `--verbose` on the command line, the parsed result is `Flag::Active`; +/// when the flag is absent, the result is `Flag::Inactive`. +/// +/// # Why not just `bool`? +/// +/// Unlike a raw `bool`, `Flag` carries **explicit semantics** about whether +/// the flag was actually provided by the user (`Active`) or simply omitted +/// (`Inactive`). This distinction matters when you want to distinguish +/// "the user intentionally omitted the flag" from "the flag was processed but +/// resolved to false" — the `Pickable` implementation for `Flag` always +/// returns `Parsed(Flag::Inactive)` when no matching argument is found, +/// rather than `NotFound`, making it always succeed with a meaningful default. +/// +/// # Conversions +/// +/// `Flag` interoperates seamlessly with `bool`: `Flag::Active` is `true`, +/// `Flag::Inactive` is `false`. The [`Deref`] impl allows using a `Flag` +/// directly in boolean contexts: +/// +/// ``` +/// # use mingling_picker::value::Flag; +/// let flag = Flag::Active; +/// if *flag { /* runs */ } +/// ``` +/// +/// [`PickerArg`]: crate::PickerArg +#[derive(Default, Clone, Copy, PartialEq, Eq)] +pub enum Flag { + /// The flag was **not** present on the command line. + /// + /// This is the default state, equivalent to `false`. + #[default] + Inactive, + + /// The flag **was** present on the command line. + /// + /// Equivalent to `true`. + Active, +} + +impl Debug for Flag { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Inactive => write!(f, "inactive"), + Self::Active => write!(f, "active"), + } + } +} + +impl Display for Flag { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Inactive => write!(f, "inactive"), + Self::Active => write!(f, "active"), + } + } +} + +impl Flag { + /// Converts this `Flag` into a `bool`. + /// + /// Returns `true` if the flag is [`Active`], `false` if [`Inactive`]. + /// + /// # Examples + /// + /// ``` + /// # use mingling_picker::value::Flag; + /// assert!(Flag::Active.bool()); + /// assert!(!Flag::Inactive.bool()); + /// ``` + /// + /// [`Active`]: Flag::Active + /// [`Inactive`]: Flag::Inactive + #[must_use] + #[inline(always)] + pub fn bool(&self) -> bool { + *self == Flag::Active + } +} + +impl PartialEq<bool> for Flag { + fn eq(&self, other: &bool) -> bool { + self.bool() == *other + } +} + +/// Compares `bool` with `Flag` using `==`. +impl PartialEq<Flag> for bool { + fn eq(&self, other: &Flag) -> bool { + *self == other.bool() + } +} + +impl From<bool> for Flag { + fn from(value: bool) -> Self { + if value { Flag::Active } else { Flag::Inactive } + } +} + +impl From<Flag> for bool { + fn from(val: Flag) -> Self { + val == Flag::Active + } +} + +/// Allows `Flag` to be used in boolean contexts via `*flag`. +/// +/// # Examples +/// +/// ``` +/// # use mingling_picker::value::Flag; +/// let flag = Flag::Active; +/// if *flag { +/// println!("flag is set"); +/// } +/// ``` +impl Deref for Flag { + type Target = bool; + + fn deref(&self) -> &bool { + match self { + Flag::Active => &true, + Flag::Inactive => &false, + } + } +} + +impl Not for Flag { + type Output = Flag; + + fn not(self) -> Flag { + match self { + Flag::Active => Flag::Inactive, + Flag::Inactive => Flag::Active, + } + } +} diff --git a/mingling_picker/src/value/vec_until.rs b/mingling_picker/src/value/vec_until.rs new file mode 100644 index 0000000..1b79641 --- /dev/null +++ b/mingling_picker/src/value/vec_until.rs @@ -0,0 +1,134 @@ +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; + +use crate::{ + BoundaryCheck, MultiPickableWithBoundary, Pickable, PickerArg, PickerArgAttr, PickerArgResult, + SinglePickable, TagPhaseContext, + matcher_needed::Matcher, + parselib::{MultiArgMatcher, ParserStyle}, +}; + +/// A `Vec`-like container that stops collecting when [`BoundaryCheck`] +/// returns `true`. +/// +/// This type exists to signal "I know what I'm doing with boundaries" +/// at the type level (as opposed to `Vec<T>` which greedily takes +/// everything). +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct VecUntil<T> { + pub(crate) inner: Vec<T>, + _marker: PhantomData<T>, +} + +impl<T> VecUntil<T> { + pub fn into_inner(self) -> Vec<T> { + self.inner + } +} + +impl<T> From<Vec<T>> for VecUntil<T> { + fn from(v: Vec<T>) -> Self { + VecUntil { + inner: v, + _marker: PhantomData, + } + } +} + +impl<T> From<VecUntil<T>> for Vec<T> { + fn from(v: VecUntil<T>) -> Self { + v.inner + } +} + +impl<T> Deref for VecUntil<T> { + type Target = Vec<T>; + fn deref(&self) -> &Vec<T> { + &self.inner + } +} + +impl<T> DerefMut for VecUntil<T> { + fn deref_mut(&mut self) -> &mut Vec<T> { + &mut self.inner + } +} + +// MultiPickableWithBoundary impl + +impl<T> MultiPickableWithBoundary for VecUntil<T> +where + T: SinglePickable + BoundaryCheck, +{ + type Checker = T; + + fn pick_multi(raw: Vec<String>) -> PickerArgResult<Self> { + let mut inner = Vec::with_capacity(raw.len()); + for s in &raw { + match T::pick_single(Some(s)) { + PickerArgResult::Parsed(v) => inner.push(v), + PickerArgResult::NotFound => return PickerArgResult::NotFound, + PickerArgResult::Unparsed => {} + } + } + PickerArgResult::Parsed(VecUntil { + inner, + _marker: PhantomData, + }) + } +} + +// Pickable impl + +impl<'a, T> Pickable<'a> for VecUntil<T> +where + T: SinglePickable + BoundaryCheck, +{ + fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr { + PickerArgAttr::positional_or_multi(flag) + } + + fn tag(ctx: TagPhaseContext) -> Vec<usize> { + let args = ctx.args; + let is_positional = ctx.arg_info.positional; + let positions = MultiArgMatcher::match_all(ctx.into()); + if positions.is_empty() { + return positions; + } + + let start = if is_positional { 0 } else { 1 }; + if start >= positions.len() { + return positions; + } + + let mut cut = start; + for &idx in &positions[start..] { + if let Some(raw) = args.get(idx) + && T::check_boundary(raw) + { + break; + } + cut += 1; + } + + positions[..cut].to_vec() + } + + fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> { + let strs = strip_flag(raw_strs); + let owned: Vec<String> = strs.iter().map(|&s| s.to_string()).collect(); + <VecUntil<T> as MultiPickableWithBoundary>::pick_multi(owned) + } +} + +/// If the first raw string looks like a named flag (starts with the +/// style's long or short prefix), strip it — it's the flag, not a value. +fn strip_flag<'a>(raw_strs: &'a [&'a str]) -> &'a [&'a str] { + if let Some(first) = raw_strs.first() { + let style = ParserStyle::global_style(); + if first.starts_with(style.long_prefix) || first.starts_with(style.short_prefix) { + return &raw_strs[1..]; + } + } + raw_strs +} diff --git a/mingling_picker/test/Cargo.lock b/mingling_picker/test/Cargo.lock new file mode 100644 index 0000000..0fef84b --- /dev/null +++ b/mingling_picker/test/Cargo.lock @@ -0,0 +1,68 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "just_fmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" + +[[package]] +name = "mingling_picker" +version = "0.3.0" +dependencies = [ + "just_fmt", + "mingling_picker_macros", +] + +[[package]] +name = "mingling_picker_macros" +version = "0.3.0" +dependencies = [ + "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.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "test-mingling-picker" +version = "0.1.0" +dependencies = [ + "mingling_picker", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/mingling_picker/test/Cargo.toml b/mingling_picker/test/Cargo.toml new file mode 100644 index 0000000..127546c --- /dev/null +++ b/mingling_picker/test/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "test-mingling-picker" +version = "0.1.0" +edition = "2024" + +[workspace] + +[dependencies] +mingling_picker = { path = "../" } diff --git a/mingling_picker/test/src/lib.rs b/mingling_picker/test/src/lib.rs new file mode 100644 index 0000000..eac6cad --- /dev/null +++ b/mingling_picker/test/src/lib.rs @@ -0,0 +1,23 @@ +// Using `assert_eq!(x, true)` is clearer than `assert!(x)` for expressing expected values +// +// BECAUSE `assert!` only checks if the boolean value is true, +// while `assert_eq!` explicitly shows the expected value +#![allow(clippy::bool_assert_comparison)] + +#[cfg(test)] +mod test; + +use mingling_picker::parselib::MaskedArg; + +/// Create a single `MaskedArg` from a raw string and its original index. +pub fn make_masked(raw: &str, idx: usize) -> MaskedArg<'_> { + MaskedArg { raw, raw_idx: idx } +} + +/// Create a `Vec<MaskedArg>` from an array of `(raw, raw_idx)` pairs. +pub fn make_args<'a>(pairs: &'a [(&'a str, usize)]) -> Vec<MaskedArg<'a>> { + pairs + .iter() + .map(|&(raw, idx)| MaskedArg { raw, raw_idx: idx }) + .collect() +} diff --git a/mingling_picker/test/src/test.rs b/mingling_picker/test/src/test.rs new file mode 100644 index 0000000..9c53514 --- /dev/null +++ b/mingling_picker/test/src/test.rs @@ -0,0 +1,10 @@ +mod arg_matcher_test; +mod basic_test; +mod multi_arg_test; +mod multi_value_test; +mod pos_matcher_test; +mod priority_test; +mod route_test; +mod style_test; +mod value_flag_test; +mod value_string_test; diff --git a/mingling_picker/test/src/test/arg_matcher_test.rs b/mingling_picker/test/src/test/arg_matcher_test.rs new file mode 100644 index 0000000..7b37bc8 --- /dev/null +++ b/mingling_picker/test/src/test/arg_matcher_test.rs @@ -0,0 +1,289 @@ +use mingling_picker::PickerArgInfo; +use mingling_picker::parselib::{ArgMatcher, Matcher, POWERSHELL_STYLE, UNIX_STYLE}; + +use crate::make_args; + +// on_match_one — Named + +#[test] +fn test_match_one_named_basic() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--name", 0), ("Alice", 1)]); + let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(0)); +} + +#[test] +fn test_match_one_named_eq_mode() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--name=Alice", 0)]); + let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(0)); +} + +#[test] +fn test_match_one_named_no_match() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--other", 0), ("Alice", 1)]); + let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +#[test] +fn test_match_one_named_short_flag() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + info.set_short('n'); + let args = make_args(&[("-n", 0), ("Alice", 1)]); + let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(0)); +} + +#[test] +fn test_match_one_named_after_end_of_options() { + // Flags after `--` should not be matched. + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--", 0), ("--name", 1), ("Alice", 2)]); + let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +// on_match_one — Positional + +#[test] +fn test_match_one_positional_basic() { + let mut info = PickerArgInfo::new(); + info.set_positional(true); + let args = make_args(&[("file.txt", 0)]); + let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(0)); +} + +#[test] +fn test_match_one_positional_takes_first() { + let mut info = PickerArgInfo::new(); + info.set_positional(true); + let args = make_args(&[("a.txt", 0), ("b.txt", 1)]); + let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(0)); +} + +// on_match_all — Named, single occurrence + +#[test] +fn test_match_all_named_flag_plus_value() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--name", 0), ("Alice", 1)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1]); +} + +#[test] +fn test_match_all_named_eq_mode() { + // --name=Alice: value is inline, only tag the flag position. + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--name=Alice", 0)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0]); +} + +#[test] +fn test_match_all_named_no_value() { + // Flag at end with no following arg: only tag the flag. + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--name", 0)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0]); +} + +#[test] +fn test_match_all_named_value_looks_like_flag() { + // The next arg looks like a flag — still tag it. + // Validation is the Pickable's responsibility. + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--name", 0), ("--other", 1)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1]); +} + +// on_match_all — Named, multiple occurrences (Single per flag) + +#[test] +fn test_match_all_named_two_occurrences() { + // --name Alice --name Bob → each occurrence gets one value. + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--name", 0), ("Alice", 1), ("--name", 2), ("Bob", 3)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1, 2, 3]); +} + +#[test] +fn test_match_all_named_skips_non_matching_args() { + // --val a b --val d → only pairs (0,1) and (3,4); idx 2 ("b") left free. + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[("--val", 0), ("a", 1), ("b", 2), ("--val", 3), ("d", 4)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1, 3, 4]); +} + +// on_match_all — Named, short flag + +#[test] +fn test_match_all_named_short_flag() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + info.set_short('n'); + let args = make_args(&[("-n", 0), ("Alice", 1)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1]); +} + +// on_match_all — Named, eq + non-eq mixed + +#[test] +fn test_match_all_named_mixed_eq_and_regular() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--name=Alice", 0), ("--name", 1), ("Bob", 2)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1, 2]); +} + +// on_match_all — Named, case insensitive (PowerShell) + +#[test] +fn test_match_all_named_powershell_case_insensitive() { + let mut info = PickerArgInfo::new(); + info.set_long("Name"); + let args = make_args(&[("-name", 0), ("Alice", 1)]); + let result = ArgMatcher::on_match_all(&args, &POWERSHELL_STYLE, &info); + assert_eq!(result, vec![0, 1]); +} + +// on_match_all — Positional + +#[test] +fn test_match_all_positional_single() { + let mut info = PickerArgInfo::new(); + info.set_positional(true); + let args = make_args(&[("file.txt", 0)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0]); +} + +#[test] +fn test_match_all_positional_multiple() { + let mut info = PickerArgInfo::new(); + info.set_positional(true); + let args = make_args(&[("a.txt", 0), ("b.txt", 1)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1]); +} + +// End-of-options marker (`--`) + +#[test] +fn test_match_all_named_stops_at_end_of_options() { + // --name before `--` should match, --name after should not. + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[ + ("--name", 0), + ("Alice", 1), + ("--", 2), + ("--name", 3), + ("Bob", 4), + ]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1]); +} + +#[test] +fn test_match_all_positional_stops_at_end_of_options() { + let mut info = PickerArgInfo::new(); + info.set_positional(true); + let args = make_args(&[("before", 0), ("--", 1), ("after", 2)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0]); +} + +// Empty args + +#[test] +fn test_match_one_empty() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = vec![]; + let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +#[test] +fn test_match_all_empty() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = vec![]; + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert!(result.is_empty()); +} + +// Verify that -- itself is never matched as a flag + +#[test] +fn test_match_all_end_of_options_not_matched() { + // `--` should neither match as a flag nor take a value. + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--", 0)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert!(result.is_empty()); +} + +// `--` should never be consumed as a value by a named flag. + +#[test] +fn test_match_all_named_does_not_consume_end_marker() { + // `--flag` before `--`, but the next position IS `--`. + // Only tag the flag, don't consume the end-of-options marker. + let mut info = PickerArgInfo::new(); + info.set_long("flag"); + let args = make_args(&[("--flag", 0), ("--", 1), ("value", 2)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0], "should NOT consume -- as a value"); +} + +#[test] +fn test_match_all_named_does_not_consume_end_marker_flag_before_end() { + // Simulates: --flag -- you where -- is end-of-options. + // The flag should be tagged but -- should NOT be consumed as its value. + let mut info = PickerArgInfo::new(); + info.set_long("flag"); + + let args = make_args(&[("--flag", 0), ("--", 1), ("you", 2)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!( + result, + vec![0], + "flag before --: only tag flag, leave -- for positional" + ); +} + +#[test] +fn test_match_all_flag_after_end_has_value() { + // Flag after `--` should not match at all. + let mut info = PickerArgInfo::new(); + info.set_long("flag"); + let args = make_args(&[("--", 0), ("--flag", 1), ("value", 2)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert!(result.is_empty()); +} diff --git a/mingling_picker/test/src/test/basic_test.rs b/mingling_picker/test/src/test/basic_test.rs new file mode 100644 index 0000000..78b154e --- /dev/null +++ b/mingling_picker/test/src/test/basic_test.rs @@ -0,0 +1,223 @@ +use mingling_picker::{IntoPicker, macros::arg}; + +// Basic bool flag — present / absent + +#[test] +fn test_bool_flag_present() { + let args = vec!["--verbose"]; + let parsed = args + .to_picker() + .pick(&arg![verbose: bool]) + .or_default() + .unwrap(); + assert_eq!(parsed, true); +} + +#[test] +fn test_bool_flag_absent() { + let args: Vec<&str> = vec![]; + let parsed = args + .to_picker() + .pick(&arg![verbose: bool]) + .or_default() + .unwrap(); + assert_eq!(parsed, false); +} + +// Short flag — '-v' + +#[test] +fn test_bool_short_flag_present() { + let args = vec!["-v"]; + let parsed = args + .to_picker() + .pick(&arg![verbose: bool, 'v']) + .or_default() + .unwrap(); + assert_eq!(parsed, true); +} + +// Multiple bool flags at once + +#[test] +fn test_two_bool_flags_both_present() { + // UNIX_STYLE now uses Kebab naming: `flag_a` → "flag-a" → --flag-a + let args = vec!["--flag-a", "--flag-b"]; + let (a, b) = args + .to_picker() + .pick(&arg![flag_a: bool]) + .or_default() + .pick(&arg![flag_b: bool]) + .or_default() + .unwrap(); + assert_eq!(a, true); + assert_eq!(b, true); +} + +#[test] +fn test_two_bool_flags_one_present() { + let args = vec!["--flag-a"]; + let (a, b) = args + .to_picker() + .pick(&arg![flag_a: bool]) + .or_default() + .pick(&arg![flag_b: bool]) + .or_default() + .unwrap(); + assert_eq!(a, true); + assert_eq!(b, false); +} + +#[test] +fn test_two_bool_flags_neither_present() { + let args: Vec<&str> = vec![]; + let (a, b) = args + .to_picker() + .pick(&arg![flag_a: bool]) + .or_default() + .pick(&arg![flag_b: bool]) + .or_default() + .unwrap(); + assert_eq!(a, false); + assert_eq!(b, false); +} + +// Mixed short and long flags + +#[test] +fn test_short_and_long_flags() { + let args = vec!["-a", "--long-b"]; + let (a, b) = args + .to_picker() + .pick(&arg![flag_a: bool, 'a']) + .or_default() + .pick(&arg![long_b: bool]) + .or_default() + .unwrap(); + assert_eq!(a, true); + assert_eq!(b, true); +} + +// Flags after `--` (end-of-options marker) should not be parsed. + +#[test] +fn test_flag_after_end_of_options() { + let args = vec!["--", "--verbose"]; + let parsed = args + .to_picker() + .pick(&arg![verbose: bool]) + .or_default() + .unwrap(); + assert_eq!(parsed, false); +} + +// Alias matching for bool flags + +#[test] +fn test_bool_flag_with_alias() { + let args = vec!["--cfg"]; + let parsed = args + .to_picker() + .pick(&arg![config: bool, "cfg"]) + .or_default() + .unwrap(); + assert_eq!(parsed, true); +} + +#[test] +fn test_bool_flag_primary_name() { + let args = vec!["--config"]; + let parsed = args + .to_picker() + .pick(&arg![config: bool, "cfg"]) + .or_default() + .unwrap(); + assert_eq!(parsed, true); +} + +// Short flag + alias for bool flag + +#[test] +fn test_bool_flag_short_and_alias() { + let args = vec!["-v"]; + let parsed = args + .to_picker() + .pick(&arg![verbose: bool, 'v', "cfg"]) + .or_default() + .unwrap(); + assert_eq!(parsed, true); +} + +// Default values: .or() / .or_default() + +#[test] +fn test_or_default_without_args() { + let args: Vec<&str> = vec![]; + let parsed = args + .to_picker() + .pick(&arg![verbose: bool]) + .or_default() + .unwrap(); + assert_eq!(parsed, false); +} + +#[test] +fn test_or_custom_default() { + let args: Vec<&str> = vec![]; + let parsed = args + .to_picker() + .pick(&arg![verbose: bool]) + .or(|| true) + .unwrap(); + assert_eq!(parsed, true); +} + +// to_result / to_option interface + +#[test] +fn test_to_result_ok() { + let args = vec!["--verbose"]; + let result = args + .to_picker() + .pick(&arg![verbose: bool]) + .or_default() + .to_result(); + assert_eq!(result, Ok(true)); +} + +#[test] +fn test_to_option_some() { + let args = vec!["--verbose"]; + let opt = args + .to_picker() + .pick(&arg![verbose: bool]) + .or_default() + .to_option(); + assert_eq!(opt, Some(true)); +} + +// Chain with_route passthrough + +#[test] +fn test_with_route_chain() { + let args = vec!["--flag"]; + let parsed = args + .with_route::<String>() + .pick(&arg![flag: bool]) + .or_default() + .unwrap(); + assert_eq!(parsed, true); +} + +// Unrelated flag should not match + +#[test] +fn test_unrelated_flag_does_not_match() { + let args = vec!["--other"]; + let parsed = args + .to_picker() + .pick(&arg![verbose: bool]) + .or_default() + .unwrap(); + assert_eq!(parsed, false); +} diff --git a/mingling_picker/test/src/test/multi_arg_test.rs b/mingling_picker/test/src/test/multi_arg_test.rs new file mode 100644 index 0000000..377357e --- /dev/null +++ b/mingling_picker/test/src/test/multi_arg_test.rs @@ -0,0 +1,181 @@ +use mingling_picker::parselib::{Matcher, MultiArgMatcher, UNIX_STYLE}; +use mingling_picker::PickerArgInfo; + +use crate::make_args; + +// on_match_one — basic + +#[test] +fn test_multi_one_finds_first_flag() { + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[("--other", 0), ("--val", 1), ("a", 2), ("b", 3)]); + let result = MultiArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(1)); +} + +#[test] +fn test_multi_one_no_match() { + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[("--other", 0)]); + let result = MultiArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +// on_match_all — named, basic multi-value + +#[test] +fn test_multi_all_flag_takes_all_values_until_next_flag() { + // --val a b c → all three values belong to --val + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[("--val", 0), ("a", 1), ("b", 2), ("c", 3)]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1, 2, 3]); +} + +#[test] +fn test_multi_all_stops_at_next_flag() { + // --val a b --other c d → only a,b belong to --val + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[("--val", 0), ("a", 1), ("b", 2), ("--other", 3), ("c", 4)]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1, 2]); +} + +#[test] +fn test_multi_all_flag_no_values() { + // --val at end with no values → just the flag + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[("--val", 0)]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0]); +} + +#[test] +fn test_multi_all_empty() { + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = vec![]; + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert!(result.is_empty()); +} + +// on_match_all — named, multiple occurrences of same flag + +#[test] +fn test_multi_all_two_occurrences() { + // --val a b --val c d → two groups + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[ + ("--val", 0), ("a", 1), ("b", 2), + ("--val", 3), ("c", 4), ("d", 5), + ]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1, 2, 3, 4, 5]); +} + +#[test] +fn test_multi_all_skips_non_matching_args() { + // --val a --other b --val c → only --val groups + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[ + ("--val", 0), ("a", 1), + ("--other", 2), ("b", 3), + ("--val", 4), ("c", 5), + ]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1, 4, 5]); +} + +// on_match_all — named, eq mode + +#[test] +fn test_multi_all_eq_mode() { + // --val=a b → eq mode + one extra value + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[("--val=a", 0), ("b", 1)]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1]); +} + +#[test] +fn test_multi_all_eq_mode_no_extra() { + // --val=a alone → just the eq flag + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[("--val=a", 0)]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0]); +} + +#[test] +fn test_multi_all_mixed_eq_and_regular() { + // --val=a b --val c d + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[ + ("--val=a", 0), ("b", 1), ("--val", 2), ("c", 3), ("d", 4), + ]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1, 2, 3, 4]); +} + +// on_match_all — short flag + +#[test] +fn test_multi_all_short_flag() { + let mut info = PickerArgInfo::new(); + info.set_short('v'); + info.set_long("val"); + let args = make_args(&[("-v", 0), ("a", 1), ("b", 2)]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1, 2]); +} + +// on_match_all — end-of-options marker + +#[test] +fn test_multi_all_stops_at_end_of_options() { + // --val a -- b → stops before `--` + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[("--val", 0), ("a", 1), ("--", 2), ("b", 3)]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1]); +} + +#[test] +fn test_multi_all_flag_after_end_ignored() { + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[("--", 0), ("--val", 1), ("a", 2)]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert!(result.is_empty()); +} + +// on_match_all — positional + +#[test] +fn test_multi_all_positional() { + let mut info = PickerArgInfo::new(); + info.set_positional(true); + let args = make_args(&[("a.txt", 0), ("b.txt", 1)]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1]); +} + +#[test] +fn test_multi_all_positional_stops_at_end_of_options() { + let mut info = PickerArgInfo::new(); + info.set_positional(true); + let args = make_args(&[("a.txt", 0), ("--", 1), ("b.txt", 2)]); + let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0]); +} diff --git a/mingling_picker/test/src/test/multi_value_test.rs b/mingling_picker/test/src/test/multi_value_test.rs new file mode 100644 index 0000000..0f56517 --- /dev/null +++ b/mingling_picker/test/src/test/multi_value_test.rs @@ -0,0 +1,66 @@ +use mingling_picker::value::{Flag, VecUntil}; +use mingling_picker::{IntoPicker, macros::arg}; + +#[test] +fn test_vec_until_i16_named() { + let (nums, rest): (VecUntil<i16>, Flag) = vec!["--nums", "1", "2", "3", "abc"] + .to_picker() + .pick(&arg![nums: VecUntil<i16>]) + .pick(&arg![rest: Flag]) + .unwrap(); + assert_eq!(*nums, vec![1i16, 2, 3]); + assert_eq!(rest, Flag::Inactive); +} + +#[test] +fn test_vec_until_i16_stops_at_non_number() { + let nums: VecUntil<i16> = vec!["--nums", "42", "abc", "100"] + .to_picker() + .pick(&arg![nums: VecUntil<i16>]) + .unwrap(); + assert_eq!(*nums, vec![42i16]); +} + +#[test] +fn test_vec_until_i16_empty() { + let nums: VecUntil<i16> = vec!["--nums"] + .to_picker() + .pick(&arg![nums: VecUntil<i16>]) + .unwrap(); + assert!(nums.is_empty()); +} + +#[test] +fn test_vec_until_i16_stops_at_next_flag() { + let nums: VecUntil<i16> = vec!["--nums", "1", "2", "--other", "3"] + .to_picker() + .pick(&arg![nums: VecUntil<i16>]) + .unwrap(); + assert_eq!(*nums, vec![1i16, 2]); +} + +#[test] +fn test_vec_until_i16_stops_at_end_of_options() { + let nums: VecUntil<i16> = vec!["--nums", "1", "2", "--", "3"] + .to_picker() + .pick(&arg![nums: VecUntil<i16>]) + .unwrap(); + assert_eq!(*nums, vec![1i16, 2]); +} + +// Two VecUntil<T> with different boundary behaviours + +#[test] +fn test_vec_until_f64_and_i16_positional() { + // Both are positional VecUntil. f64 takes all valid floats, + // i16 takes what's left. But note: "1", "2", "3" are also valid + // f64 values, so f64's check_boundary never fires — it consumes + // everything, and i16 gets nothing. + let (floats, ints): (VecUntil<f64>, VecUntil<i16>) = vec!["1.5", "2.5", "3.5", "1", "2", "3"] + .to_picker() + .pick(&arg![VecUntil<f64>]) + .pick(&arg![VecUntil<i16>]) + .unwrap(); + assert_eq!(*floats, vec![1.5, 2.5, 3.5]); + assert_eq!(*ints, vec![1i16, 2, 3]); +} diff --git a/mingling_picker/test/src/test/pos_matcher_test.rs b/mingling_picker/test/src/test/pos_matcher_test.rs new file mode 100644 index 0000000..b5319f5 --- /dev/null +++ b/mingling_picker/test/src/test/pos_matcher_test.rs @@ -0,0 +1,141 @@ +use mingling_picker::PickerArgInfo; +use mingling_picker::parselib::{MaskedArg, Matcher, PositionalMatcher, UNIX_STYLE, WINDOWS_STYLE}; + +fn make_args<'a>(pairs: &'a [(&'a str, usize)]) -> Vec<MaskedArg<'a>> { + pairs + .iter() + .map(|&(raw, idx)| MaskedArg { raw, raw_idx: idx }) + .collect() +} + +// on_match_one — basic + +#[test] +fn test_pos_one_takes_first_non_flag() { + let info = PickerArgInfo::new(); + let args = make_args(&[("--verbose", 0), ("file.txt", 1)]); + let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(1)); +} + +#[test] +fn test_pos_one_takes_first_if_no_flag() { + let info = PickerArgInfo::new(); + let args = make_args(&[("file.txt", 0)]); + let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(0)); +} + +#[test] +fn test_pos_one_all_flags_returns_none() { + let info = PickerArgInfo::new(); + let args = make_args(&[("--verbose", 0), ("--name", 1)]); + let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +#[test] +fn test_pos_one_empty_returns_none() { + let info = PickerArgInfo::new(); + let args = vec![]; + let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +// on_match_one — after `--` + +#[test] +fn test_pos_one_after_end_takes_even_flag_like() { + // After `--`, accept everything including `--verbose`. + let info = PickerArgInfo::new(); + let args = make_args(&[("--", 0), ("--verbose", 1), ("file.txt", 2)]); + let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(1)); +} + +#[test] +fn test_pos_one_only_end_returns_none() { + let info = PickerArgInfo::new(); + let args = make_args(&[("--", 0)]); + let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +// on_match_one — Windows style prefix + +#[test] +fn test_pos_one_windows_skips_slash_prefix() { + let info = PickerArgInfo::new(); + let args = make_args(&[("/Verbose", 0), ("file.txt", 1)]); + let result = PositionalMatcher::on_match_one(&args, &WINDOWS_STYLE, &info); + assert_eq!(result, Some(1)); +} + +// on_match_all — basic + +#[test] +fn test_pos_all_collects_non_flags() { + let info = PickerArgInfo::new(); + let args = make_args(&[("--verbose", 0), ("a.txt", 1), ("--name", 2), ("b.txt", 3)]); + let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![1, 3]); +} + +#[test] +fn test_pos_all_only_flags_returns_empty() { + let info = PickerArgInfo::new(); + let args = make_args(&[("--a", 0), ("--b", 1)]); + let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert!(result.is_empty()); +} + +#[test] +fn test_pos_all_empty() { + let info = PickerArgInfo::new(); + let args = vec![]; + let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert!(result.is_empty()); +} + +// on_match_all — after `--` + +#[test] +fn test_pos_all_after_end_accepts_everything() { + // After `--`, even `--verbose` is accepted as positional. + let info = PickerArgInfo::new(); + let args = make_args(&[ + ("--verbose", 0), + ("a.txt", 1), + ("--", 2), + ("--flag", 3), + ("b.txt", 4), + ]); + let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![1, 3, 4]); +} + +#[test] +fn test_pos_all_only_after_end() { + let info = PickerArgInfo::new(); + let args = make_args(&[("--", 0), ("arg1", 1), ("--arg2", 2)]); + let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![1, 2]); +} + +// on_match_all — mixed before/after `--` + +#[test] +fn test_pos_all_mixed() { + let info = PickerArgInfo::new(); + let args = make_args(&[ + ("infile", 0), + ("--verbose", 1), + ("--", 2), + ("outfile", 3), + ("--extra", 4), + ]); + let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); + // Before `--`: "infile" (skip --verbose). + // After `--`: everything — "outfile", "--extra". + assert_eq!(result, vec![0, 3, 4]); +} diff --git a/mingling_picker/test/src/test/priority_test.rs b/mingling_picker/test/src/test/priority_test.rs new file mode 100644 index 0000000..8179389 --- /dev/null +++ b/mingling_picker/test/src/test/priority_test.rs @@ -0,0 +1,85 @@ +use mingling_picker::value::Flag; +use mingling_picker::{IntoPicker, macros::arg}; + +// Same flag name, different Pickable types +// +// PickerArgAttr priority: Flag < Single +// So Single and Multi should parse BEFORE Flag when sharing +// the same flag name. + +#[test] +fn test_single_takes_flag_when_sharing_name() { + // --name Alice: String consumes it, Flag sees nothing. + let (name, verbose): (String, Flag) = vec!["--name", "Alice"] + .to_picker() + .pick(&arg![name: String]) // Single, parsed first + .pick(&arg![name: Flag]) // Flag, parsed second, nothing left + .unwrap(); + assert_eq!(name, "Alice"); + assert_eq!(verbose, Flag::Inactive); +} + +#[test] +fn test_flag_only_triggers_when_single_missing() { + // --verbose: only Flag matches, String gets default. + let (name, verbose): (String, Flag) = vec!["--verbose"] + .to_picker() + .pick(&arg![name: String]) // Single, no match → default "" + .or_default() + .pick(&arg![name: Flag]) // Flag, no --name in args → Inactive + .unwrap(); + assert_eq!(name, ""); + assert_eq!(verbose, Flag::Inactive); +} + +#[test] +fn test_flag_gets_leftovers_after_single_consumes_value() { + // --name Alice --name: String takes "--name Alice", Flag takes "--name". + let (name, verbose): (String, Flag) = vec!["--name", "Alice", "--name"] + .to_picker() + .pick(&arg![name: String]) // Single: tag [0, 1], consumes --name Alice + .pick(&arg![name: Flag]) // Flag: tags position 2 (--name) + .unwrap(); + assert_eq!(name, "Alice"); + assert_eq!( + verbose, + Flag::Active, + "Flag should see --name at position 2 after String consumed positions 0-1" + ); +} + +#[test] +fn test_short_flag_sharing_same_letter() { + // -n Alice: String takes it, Flag misses. + let (name, verbose): (String, Flag) = vec!["-n", "Alice"] + .to_picker() + .pick(&arg![name: String, 'n']) // Single, parsed first + .pick(&arg![name: Flag, 'n']) // Flag, parsed second + .unwrap(); + assert_eq!(name, "Alice"); + assert_eq!(verbose, Flag::Inactive); +} + +#[test] +fn test_flag_captures_remaining_after_single_partial_consume() { + // --name Alice --verbose: String takes --name Alice, Flag takes --verbose. + let (name, verbose): (String, Flag) = vec!["--name", "Alice", "--verbose"] + .to_picker() + .pick(&arg![name: String]) // Single: tag [0, 1] + .pick(&arg![verbose: Flag]) // Flag: tag [2] + .unwrap(); + assert_eq!(name, "Alice"); + assert_eq!(verbose, Flag::Active); +} + +#[test] +fn test_single_skips_already_claimed_positions() { + // --verbose --name Alice: Flag takes --verbose, String takes --name Alice. + let (verbose, name): (Flag, String) = vec!["--verbose", "--name", "Alice"] + .to_picker() + .pick(&arg![verbose: Flag]) // Flag: tag [0] + .pick(&arg![name: String]) // Single: tag [1, 2] + .unwrap(); + assert_eq!(verbose, Flag::Active); + assert_eq!(name, "Alice"); +} diff --git a/mingling_picker/test/src/test/route_test.rs b/mingling_picker/test/src/test/route_test.rs new file mode 100644 index 0000000..9261db1 --- /dev/null +++ b/mingling_picker/test/src/test/route_test.rs @@ -0,0 +1,200 @@ +use mingling_picker::{IntoPicker, macros::arg}; + +// Route mechanism — or_route + +#[test] +fn test_or_route_triggered_to_result() { + // flag not present and no default value → route triggered → Err + let args: Vec<&str> = vec![]; + let result: Result<bool, &'static str> = args + .with_route::<&'static str>() + .pick(&arg![verbose: bool]) + .or_route(|| "missing_verbose") + .to_result(); + assert_eq!(result, Err("missing_verbose")); +} + +#[test] +fn test_or_route_not_triggered_when_flag_present() { + // flag present → route not triggered → Ok + let args = vec!["--verbose"]; + let result: Result<bool, &'static str> = args + .with_route::<&'static str>() + .pick(&arg![verbose: bool]) + .or_route(|| "missing_verbose") + .to_result(); + assert_eq!(result, Ok(true)); +} + +#[test] +fn test_or_default_priority_over_or_route() { + // or_default takes priority over or_route: even if the flag is absent, + // having a default prevents the route from being triggered + let args: Vec<&str> = vec![]; + let result: Result<bool, &'static str> = args + .with_route::<&'static str>() + .pick(&arg![verbose: bool]) + .or_default() + .or_route(|| "should_not_reach") + .to_result(); + assert_eq!(result, Ok(false)); +} + +#[test] +fn test_or_route_to_option_returns_none() { + // When route is triggered, to_option returns None + let args: Vec<&str> = vec![]; + let opt: Option<bool> = args + .with_route::<&'static str>() + .pick(&arg![verbose: bool]) + .or_route(|| "missing") + .to_option(); + assert_eq!(opt, None); +} + +#[test] +#[should_panic(expected = "called `Option::unwrap()` on a `None` value")] +fn test_or_route_unwrap_panics() { + let args: Vec<&str> = vec![]; + args.with_route::<&'static str>() + .pick(&arg![verbose: bool]) + .or_route(|| "missing") + .unwrap(); +} + +#[test] +fn test_or_route_with_string_route() { + // Route type is String + let args: Vec<&str> = vec![]; + let result: Result<bool, String> = args + .with_route::<String>() + .pick(&arg![verbose: bool]) + .or_route(|| "route_hit".to_string()) + .to_result(); + assert_eq!(result, Err("route_hit".to_string())); +} + +#[test] +fn test_or_route_with_custom_enum() { + // Route type is a custom enum + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + #[allow(dead_code)] + enum Redirect { + Help, + Version, + } + + let args: Vec<&str> = vec![]; + let result: Result<bool, Redirect> = args + .with_route::<Redirect>() + .pick(&arg![verbose: bool]) + .or_route(|| Redirect::Help) + .to_result(); + assert_eq!(result, Err(Redirect::Help)); +} + +// Route mechanism — multiple flags + +#[test] +fn test_two_flags_first_missing_triggers_route() { + // Both flags missing; first has or_route → Err("first"); + // first route wins (first-checked, first-triggered) + let args: Vec<&str> = vec![]; + let result: Result<(bool, bool), &'static str> = args + .with_route::<&'static str>() + .pick(&arg![flag_a: bool]) + .or_route(|| "first_missing") + .pick(&arg![flag_b: bool]) + .or_route(|| "second_missing") + .to_result(); + assert_eq!(result, Err("first_missing")); +} + +#[test] +fn test_two_flags_second_missing_triggers_route() { + // First has or_default (no route triggered), second missing with or_route → Err("second") + let args: Vec<&str> = vec![]; + let result: Result<(bool, bool), &'static str> = args + .with_route::<&'static str>() + .pick(&arg![flag_a: bool]) + .or_default() + .pick(&arg![flag_b: bool]) + .or_route(|| "second_missing") + .to_result(); + assert_eq!(result, Err("second_missing")); +} + +#[test] +fn test_two_flags_both_present_route_not_triggered() { + // Both flags present → route not triggered → Ok((true, true)) + let args = vec!["--flag-a", "--flag-b"]; + let result: Result<(bool, bool), &'static str> = args + .with_route::<&'static str>() + .pick(&arg![flag_a: bool]) + .or_route(|| "first_missing") + .pick(&arg![flag_b: bool]) + .or_route(|| "second_missing") + .to_result(); + assert_eq!(result, Ok((true, true))); +} + +#[test] +fn test_two_flags_first_missing_no_route_second_has_route() { + // First missing but has no or_route, second missing with or_route → Err("second") + // Note: the first has neither route nor default, so pick failure does not modify error_route + let args: Vec<&str> = vec![]; + let result: Result<(bool, bool), &'static str> = args + .with_route::<&'static str>() + .pick(&arg![flag_a: bool]) + // No or_default, no or_route either + .pick(&arg![flag_b: bool]) + .or_route(|| "second_missing") + .to_result(); + assert_eq!(result, Err("second_missing")); +} + +#[test] +fn test_two_flags_only_second_has_default() { + // First is missing with no default/route (v1=NotFound), second is missing but has or_default + // Use unpack to check both results + let args: Vec<&str> = vec![]; + let (a, b) = args + .to_picker() + .pick(&arg![flag_a: bool]) + .pick(&arg![flag_b: bool]) + .or_default() + .unpack(); + assert_eq!(a, None); + assert_eq!(b, Some(false)); +} + +// Route with with_route + +#[test] +fn test_with_route_and_or_route() { + // with_route::<String>() sets the Route type + or_route triggers + let args: Vec<&str> = vec![]; + let result: Result<bool, String> = args + .with_route::<String>() + .pick(&arg![verbose: bool]) + .or_route(|| "redirected".to_string()) + .to_result(); + assert_eq!(result, Err("redirected".to_string())); +} + +#[test] +fn test_with_route_type_switch() { + // with_route switching Route type clears old route_$ and error_route + let args: Vec<&str> = vec![]; + let result: Result<(bool, bool), i32> = args + .with_route::<String>() + .pick(&arg![verbose: bool]) + .or_route(|| "string_route".to_string()) + .with_route::<i32>() + .pick(&arg![other: bool]) + .or_route(|| -1) + .to_result(); + // The first flag is missing, but its or_route is cleared when with_route switches (route_$ = None) + // The second flag is missing and has or_route → Err(-1) + assert_eq!(result, Err(-1)); +} diff --git a/mingling_picker/test/src/test/style_test.rs b/mingling_picker/test/src/test/style_test.rs new file mode 100644 index 0000000..3c337b9 --- /dev/null +++ b/mingling_picker/test/src/test/style_test.rs @@ -0,0 +1,300 @@ +use mingling_picker::PickerArgInfo; +use mingling_picker::parselib::{ + FlagMatcher, Matcher, POWERSHELL_STYLE, ParserStyle, ParserStyleNamingCase, UNIX_STYLE, + WINDOWS_STYLE, build_possible_flags, +}; + +use crate::make_masked; + +// Style: formatting utilities + +#[test] +fn test_unix_style_flag_string() { + assert_eq!(UNIX_STYLE.flag_string('v'), "-v"); + assert_eq!(UNIX_STYLE.flag_string("verbose"), "--verbose"); +} + +#[test] +fn test_windows_style_flag_string() { + assert_eq!(WINDOWS_STYLE.flag_string('v'), "/v"); + assert_eq!(WINDOWS_STYLE.flag_string("verbose"), "/verbose"); +} + +#[test] +fn test_powershell_style_flag_string() { + assert_eq!(POWERSHELL_STYLE.flag_string('v'), "-v"); + assert_eq!(POWERSHELL_STYLE.flag_string("Verbose"), "-Verbose"); +} + +#[test] +fn test_build_possible_flags_windows() { + // Build PickerArgInfo from a flag definition: `verbose: bool` + let mut info = PickerArgInfo::new(); + info.set_long("verbose"); + let flags = build_possible_flags(&WINDOWS_STYLE, &info); + assert_eq!(flags, vec!["/Verbose"]); +} + +#[test] +fn test_build_possible_flags_with_short_and_alias() { + let mut info = PickerArgInfo::new(); + info.set_short('n'); + info.set_long("name"); + info.set_alias(vec!["nickname"]); + let flags = build_possible_flags(&UNIX_STYLE, &info); + assert_eq!(flags, vec!["-n", "--name", "--nickname"]); +} + +// Style: matching with different styles via Matcher trait + +#[test] +fn test_windows_style_match() { + // Windows style: /verbose (case insensitive) + let mut info = PickerArgInfo::new(); + info.set_long("verbose"); + + let args = vec![make_masked("/verbose", 0)]; + let result = FlagMatcher::on_match_one(&args, &WINDOWS_STYLE, &info); + assert_eq!(result, Some(0)); +} + +#[test] +fn test_windows_style_match_case_insensitive() { + // Windows style is case-insensitive: /VERBOSE should match "verbose" + let mut info = PickerArgInfo::new(); + info.set_long("verbose"); + + let args = vec![make_masked("/VERBOSE", 0)]; + let result = FlagMatcher::on_match_one(&args, &WINDOWS_STYLE, &info); + assert_eq!(result, Some(0)); +} + +#[test] +fn test_windows_style_no_match_on_unrelated_flag() { + // Different flag should not match + let mut info = PickerArgInfo::new(); + info.set_long("verbose"); + + let args = vec![make_masked("/output", 0)]; + let result = FlagMatcher::on_match_one(&args, &WINDOWS_STYLE, &info); + assert_eq!(result, None); +} + +#[test] +fn test_powershell_style_match() { + // PowerShell style: -Verbose (case insensitive) + let mut info = PickerArgInfo::new(); + info.set_long("Verbose"); + + let args = vec![make_masked("-Verbose", 0)]; + let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info); + assert_eq!(result, Some(0)); +} + +#[test] +fn test_powershell_style_match_case_insensitive() { + let mut info = PickerArgInfo::new(); + info.set_long("Verbose"); + + let args = vec![make_masked("-VERBOSE", 0)]; + let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info); + assert_eq!(result, Some(0)); +} + +#[test] +fn test_unix_style_case_sensitive_no_match() { + // UNIX style is case-sensitive: --VERBOSE should NOT match --verbose + let mut info = PickerArgInfo::new(); + info.set_long("verbose"); + + let args = vec![make_masked("--VERBOSE", 0)]; + let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +#[test] +fn test_windows_style_match_all() { + // on_match_all should find all matching flags + let mut info = PickerArgInfo::new(); + info.set_long("verbose"); + info.set_short('v'); + + let args = vec![ + make_masked("/v", 0), + make_masked("/output", 1), + make_masked("/VERBOSE", 2), + ]; + let result = FlagMatcher::on_match_all(&args, &WINDOWS_STYLE, &info); + assert_eq!(result, vec![0, 2]); +} + +#[test] +fn test_windows_style_match_after_end_of_options() { + // end_of_options is always "--" regardless of style + // Flags after -- should not match + let mut info = PickerArgInfo::new(); + info.set_long("verbose"); + + let args = vec![ + make_masked("/verbose", 0), + make_masked("--", 1), + make_masked("/verbose", 2), + ]; + let result = FlagMatcher::on_match_all(&args, &WINDOWS_STYLE, &info); + // end_of_options is always "--" regardless of style + assert_eq!(result, vec![0]); +} + +// Naming case conversion + +/// A Unix-like style with kebab-case naming. +const KEBAB_STYLE: ParserStyle<'static> = ParserStyle { + naming_case: ParserStyleNamingCase::Kebab, + ..UNIX_STYLE +}; + +#[test] +fn test_kebab_case_naming_for_multiword_flag() { + // flag name `flag_a` → Kebab → `flag-a` → `--flag-a` + let mut info = PickerArgInfo::new(); + info.set_long("flag_a"); + + let args = vec![make_masked("--flag-a", 0)]; + let result = FlagMatcher::on_match_one(&args, &KEBAB_STYLE, &info); + assert_eq!( + result, + Some(0), + "--flag-a should match flag_a via kebab-case conversion" + ); +} + +#[test] +fn test_snake_case_should_not_match_as_long_flag() { + // With kebab naming, `--flag_a` (snake) should NOT match `flag_a` + let mut info = PickerArgInfo::new(); + info.set_long("flag_a"); + + let args = vec![make_masked("--flag_a", 0)]; + let result = FlagMatcher::on_match_one(&args, &KEBAB_STYLE, &info); + assert_eq!( + result, None, + "--flag_a should NOT match in kebab-style context" + ); +} + +#[test] +fn test_kebab_case_naming_for_unix_style() { + // UNIX_STYLE now uses Kebab case: `flag_a` → `flag-a` → `--flag-a` + let mut info = PickerArgInfo::new(); + info.set_long("flag_a"); + + let args = vec![make_masked("--flag-a", 0)]; + let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!( + result, + Some(0), + "--flag-a should match with kebab-style UNIX_STYLE" + ); +} + +#[test] +fn test_snake_case_rejected_by_unix_style() { + // UNIX_STYLE uses Kebab: `--flag_a` (snake) should NOT match + let mut info = PickerArgInfo::new(); + info.set_long("flag_a"); + + let args = vec![make_masked("--flag_a", 0)]; + let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!( + result, None, + "--flag_a should NOT match under kebab-style UNIX_STYLE" + ); +} + +#[test] +fn test_powershell_pascal_case_naming() { + // POWERSHELL_STYLE uses Pascal case: `verbose` → `Verbose` → `-Verbose` + let mut info = PickerArgInfo::new(); + info.set_long("verbose"); + + let args = vec![make_masked("-Verbose", 0)]; + let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info); + assert_eq!( + result, + Some(0), + "-Verbose should match verbose via Pascal case" + ); +} + +#[test] +fn test_flag_naming_kebab_matches_my_name() { + // UNIX_STYLE now uses Kebab: `my_name` → `my-name` → `--my-name` + let mut info = PickerArgInfo::new(); + info.set_long("my_name"); + + let args = vec![make_masked("--my-name", 0)]; + let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!( + result, + Some(0), + "--my-name should match via kebab conversion" + ); +} + +#[test] +fn test_flag_naming_kebab_rejects_my_name_underscore() { + // Kebab style: `--my_name` (snake) should NOT match + let mut info = PickerArgInfo::new(); + info.set_long("my_name"); + + let args = vec![make_masked("--my_name", 0)]; + let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!( + result, None, + "--my_name should NOT match under kebab-style UNIX_STYLE" + ); +} + +#[test] +fn test_flag_naming_pascal_matches_my_name() { + // `my_name` under Pascal → `MyName` → `-MyName` + let mut info = PickerArgInfo::new(); + info.set_long("my_name"); + + let args = vec![make_masked("-MyName", 0)]; + let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info); + assert_eq!( + result, + Some(0), + "-MyName should match via Pascal conversion" + ); +} + +#[test] +fn test_flag_naming_pascal_matches_lowercase() { + // PowerShell is case-insensitive: `-myname` should also match + let mut info = PickerArgInfo::new(); + info.set_long("my_name"); + + let args = vec![make_masked("-myname", 0)]; + let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info); + assert_eq!( + result, + Some(0), + "-myname (lowercase) should match via case-insensitive Pascal" + ); +} + +#[test] +fn test_flag_naming_pascal_rejects_my_name_underscore() { + // Pascal style: `-my_name` (underscore) should NOT match + let mut info = PickerArgInfo::new(); + info.set_long("my_name"); + + let args = vec![make_masked("-my_name", 0)]; + let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info); + assert_eq!( + result, None, + "-my_name should NOT match under Pascal-style naming" + ); +} diff --git a/mingling_picker/test/src/test/value_flag_test.rs b/mingling_picker/test/src/test/value_flag_test.rs new file mode 100644 index 0000000..d267a27 --- /dev/null +++ b/mingling_picker/test/src/test/value_flag_test.rs @@ -0,0 +1,174 @@ +use mingling_picker::value::Flag; +use mingling_picker::{IntoPicker, macros::arg}; + +// Basic Flag — present / absent + +#[test] +fn test_flag_present() { + let flag: Flag = vec!["--verbose"] + .to_picker() + .pick(&arg![verbose: Flag]) + .unwrap(); + assert_eq!(flag, Flag::Active); +} + +#[test] +fn test_flag_absent_returns_inactive() { + // Unlike bool, Flag::pick returns Parsed(Inactive) when no match is found, + // so or_default() is NOT required — unwrap() works directly. + let flag: Flag = Vec::<&str>::new() + .to_picker() + .pick(&arg![verbose: Flag]) + .unwrap(); + assert_eq!(flag, Flag::Inactive); +} + +// Short Flag + +#[test] +fn test_flag_short_present() { + let flag: Flag = vec!["-v"] + .to_picker() + .pick(&arg![verbose: Flag, 'v']) + .unwrap(); + assert_eq!(flag, Flag::Active); +} + +// Multiple Flags + +#[test] +fn test_two_flags_both_present() { + let (a, b): (Flag, Flag) = vec!["--flag-a", "--flag-b"] + .to_picker() + .pick(&arg![flag_a: Flag]) + .pick(&arg![flag_b: Flag]) + .unwrap(); + assert_eq!(a, Flag::Active); + assert_eq!(b, Flag::Active); +} + +#[test] +fn test_two_flags_one_present() { + let (a, b): (Flag, Flag) = vec!["--flag-a"] + .to_picker() + .pick(&arg![flag_a: Flag]) + .pick(&arg![flag_b: Flag]) + .unwrap(); + assert_eq!(a, Flag::Active); + assert_eq!(b, Flag::Inactive); +} + +#[test] +fn test_two_flags_neither_present() { + let (a, b): (Flag, Flag) = Vec::<&str>::new() + .to_picker() + .pick(&arg![flag_a: Flag]) + .pick(&arg![flag_b: Flag]) + .unwrap(); + assert_eq!(a, Flag::Inactive); + assert_eq!(b, Flag::Inactive); +} + +// After `--` (end-of-options) + +#[test] +fn test_flag_after_end_of_options() { + let flag: Flag = vec!["--", "--verbose"] + .to_picker() + .pick(&arg![verbose: Flag]) + .unwrap(); + assert_eq!(flag, Flag::Inactive); +} + +// Alias + +#[test] +fn test_flag_with_alias() { + let flag: Flag = vec!["--cfg"] + .to_picker() + .pick(&arg![config: Flag, "cfg"]) + .unwrap(); + assert_eq!(flag, Flag::Active); +} + +#[test] +fn test_flag_primary_name() { + let flag: Flag = vec!["--config"] + .to_picker() + .pick(&arg![config: Flag, "cfg"]) + .unwrap(); + assert_eq!(flag, Flag::Active); +} + +// Unrelated flag should not match + +#[test] +fn test_unrelated_flag_does_not_match() { + let flag: Flag = vec!["--other"] + .to_picker() + .pick(&arg![verbose: Flag]) + .unwrap(); + assert_eq!(flag, Flag::Inactive); +} + +// to_result / to_option + +#[test] +fn test_flag_to_result() { + let result: Result<Flag, ()> = vec!["--verbose"] + .to_picker() + .pick(&arg![verbose: Flag]) + .to_result(); + assert_eq!(result, Ok(Flag::Active)); +} + +#[test] +fn test_flag_to_option() { + let opt: Option<Flag> = vec!["--verbose"] + .to_picker() + .pick(&arg![verbose: Flag]) + .to_option(); + assert_eq!(opt, Some(Flag::Active)); +} + +// Bool conversions + +#[test] +fn test_flag_converts_to_bool() { + let flag = Flag::Active; + assert!(bool::from(flag)); + + let flag = Flag::Inactive; + assert!(!bool::from(flag)); +} + +#[test] +fn test_flag_from_bool() { + assert_eq!(Flag::from(true), Flag::Active); + assert_eq!(Flag::from(false), Flag::Inactive); +} + +#[test] +fn test_flag_deref_to_bool() { + let active = Flag::Active; + assert!(*active); + + let inactive = Flag::Inactive; + assert!(!*inactive); +} + +// Flag never triggers route (unlike bool) +// +// Flag::pick always returns Parsed, so the fallback chain +// (default → route) is never entered. + +#[test] +fn test_flag_absent_does_not_trigger_route() { + // Even without or_default / or_route, absent flag returns Inactive, not a route + let result: Result<Flag, &str> = Vec::<&str>::new() + .with_route::<&str>() + .pick(&arg![verbose: Flag]) + .or_route(|| "should_not_fire") + .to_result(); + assert_eq!(result, Ok(Flag::Inactive)); +} diff --git a/mingling_picker/test/src/test/value_string_test.rs b/mingling_picker/test/src/test/value_string_test.rs new file mode 100644 index 0000000..b1e5c0b --- /dev/null +++ b/mingling_picker/test/src/test/value_string_test.rs @@ -0,0 +1,171 @@ +use mingling_picker::{IntoPicker, macros::arg}; + +// Basic named String — present / absent + +#[test] +fn test_string_named_present() { + let val: String = vec!["--name", "Alice"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .unwrap(); + assert_eq!(val, "Alice"); +} + +#[test] +fn test_string_named_absent_uses_default() { + let val: String = Vec::<&str>::new() + .to_picker() + .pick(&arg![name: String]) + .or_default() + .unwrap(); + assert_eq!(val, ""); +} + +// Named String — eq mode + +#[test] +fn test_string_named_eq_mode() { + let val: String = vec!["--name=Alice"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .unwrap(); + assert_eq!(val, "Alice"); +} + +// Named String — short flag + +#[test] +fn test_string_named_short_flag() { + let val: String = vec!["-n", "Alice"] + .to_picker() + .pick(&arg![name: String, 'n']) + .or_default() + .unwrap(); + assert_eq!(val, "Alice"); +} + +// Named String — no value after flag + +#[test] +fn test_string_named_missing_value_triggers_default() { + // --name at end with no following arg → pick returns NotFound → or_default gives "" + let val: String = vec!["--name"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .unwrap(); + assert_eq!(val, ""); +} + +// Positional String + +#[test] +fn test_string_positional() { + let val: String = vec!["file.txt"] + .to_picker() + .pick(&arg![String]) + .or_default() + .unwrap(); + assert_eq!(val, "file.txt"); +} + +#[test] +fn test_string_positional_takes_first() { + let val: String = vec!["first", "second"] + .to_picker() + .pick(&arg![String]) + .or_default() + .unwrap(); + assert_eq!(val, "first"); +} + +// Multiple occurrences (Single only tags one occurrence per Parseable) + +#[test] +fn test_string_two_named_flags() { + let (a, b): (String, String) = vec!["--name", "Alice", "--greeting", "Hello"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .pick(&arg![greeting: String]) + .or_default() + .unwrap(); + assert_eq!(a, "Alice"); + assert_eq!(b, "Hello"); +} + +// Mixed named + positional + +#[test] +fn test_string_named_and_positional() { + let (name, file): (String, String) = vec!["--name", "Alice", "file.txt"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .pick(&arg![String]) + .or_default() + .unwrap(); + assert_eq!(name, "Alice"); + assert_eq!(file, "file.txt"); +} + +// After `--` (end-of-options) + +#[test] +fn test_string_named_after_end_of_options() { + // Named arg after `--` should not be matched → default + let val: String = vec!["--", "--name", "Alice"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .unwrap(); + assert_eq!(val, ""); +} + +// Unrelated flag should not match → default + +#[test] +fn test_string_unrelated_flag() { + let val: String = vec!["--other", "value"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .unwrap(); + assert_eq!(val, ""); +} + +// to_result / to_option + +#[test] +fn test_string_to_result() { + let result: Result<String, ()> = vec!["--name", "Alice"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .to_result(); + assert_eq!(result, Ok("Alice".to_string())); +} + +#[test] +fn test_string_to_option() { + let opt: Option<String> = vec!["--name", "Alice"] + .to_picker() + .pick(&arg![name: String]) + .or_default() + .to_option(); + assert_eq!(opt, Some("Alice".to_string())); +} + +// Custom default via .or() + +#[test] +fn test_string_custom_default() { + let val: String = Vec::<&str>::new() + .to_picker() + .pick(&arg![name: String]) + .or(|| "default_name".to_string()) + .unwrap(); + assert_eq!(val, "default_name"); +} diff --git a/mingling_picker_macros/Cargo.toml b/mingling_picker_macros/Cargo.toml new file mode 100644 index 0000000..58488ea --- /dev/null +++ b/mingling_picker_macros/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "mingling_picker_macros" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +authors = ["Weicao-CatilGrass"] +readme = "README.md" +description = "Mingling's lightweight argument parser macros" + +[lib] +proc-macro = true + +[features] +mingling_support = [] + +[dependencies] +syn.workspace = true +quote.workspace = true +proc-macro2.workspace = true diff --git a/mingling_picker_macros/README.md b/mingling_picker_macros/README.md new file mode 100644 index 0000000..5a3f220 --- /dev/null +++ b/mingling_picker_macros/README.md @@ -0,0 +1,40 @@ +# Mingling Picker Macros + +Procedural macros for [Mingling Picker](https://github.com/mingling-rs/mingling/tree/main/mingling_picker), enabled by the `mingling/picker` feature. + +```toml +[dependencies.mingling] +version = "0.3.0" +features = [ + "picker" +] +``` + +## Provided Macros + +### Macro `arg!` + +Declares a parameter definition for use with `Picker`'s `.pick()` method: + +```rust,ignore +use mingling_picker_macros::arg; + +// Named flag with a value +let flag = arg![name: String]; + +// Named flag with short form +let flag = arg![name: String, 'n']; + +// Named flag with alias +let flag = arg![name: String, 'n', "nickname"]; + +// Positional parameter +let flag = arg![String]; + +// Flag-only parameter (boolean) +let flag = arg![verbose: Flag]; +``` + +### Macro `internal_repeat!` (Internal) + +Internal macro used by Picker to generate `PickerPattern1..=32` and their parsing logic. Not intended for direct use. diff --git a/mingling_picker_macros/src/arg.rs b/mingling_picker_macros/src/arg.rs new file mode 100644 index 0000000..70b27b0 --- /dev/null +++ b/mingling_picker_macros/src/arg.rs @@ -0,0 +1,205 @@ +use proc_macro::{TokenStream, TokenTree}; +use proc_macro2::TokenStream as TS2; +use quote::quote; + +pub(crate) fn arg(input: TokenStream) -> TokenStream { + let tokens: Vec<TokenTree> = input.into_iter().collect(); + let args = split_at_commas(&tokens); + if args.is_empty() { + return quote! { compile_error!("arg! flaguires at least one argument") }.into(); + } + + let first = &args[0]; + let rest = &args[1..]; + + // Validate: at most one char literal + let char_count = rest.iter().filter(|a| is_char_literal(a)).count(); + if char_count > 1 { + return quote! { compile_error!("arg! only supports at most one short name") }.into(); + } + + // Extract short char and string aliases + let short_char: Option<char> = rest + .iter() + .find(|a| is_char_literal(a)) + .and_then(|a| extract_char(a)); + let aliases: Vec<String> = rest + .iter() + .filter(|a| is_string_literal(a)) + .filter_map(|a| extract_string(a)) + .collect(); + + // Parse first argument + // arg![name: Type] → name, with explicit type + // arg![Type] → positional type, no name + let colon_pos = first + .iter() + .position(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == ':')); + let has_named_colon = colon_pos.is_some_and(|pos| pos > 0 && !is_double_colon(first, pos)); + + let first_slice = first.as_slice(); + let (name, ty, is_named) = if has_named_colon { + let pos = colon_pos.unwrap(); + // `name : Type` + ( + Some(&first_slice[..pos]), + Some(&first_slice[pos + 1..]), + true, + ) + } else { + // No `name:` prefix → the entire first argument is the type + (None, Some(first_slice), false) + }; + + // Build full names + let full_names: Vec<String> = match name { + Some(n) => { + let mut v = vec![join_idents(n)]; + v.extend(aliases); + v + } + None => aliases, + }; + + let path: TS2 = { + let ty_ts: TS2 = ty + .map(|t| { + let ts: TokenStream = t.iter().cloned().collect(); + TS2::from(ts) + }) + .unwrap_or(TS2::new()); + + #[cfg(feature = "mingling_support")] + let import = quote! { ::mingling::picker::PickerArg }; + + #[cfg(not(feature = "mingling_support"))] + let import = quote! { ::mingling_picker::PickerArg }; + + if ty.is_some() { + quote! { #import::<#ty_ts> } + } else { + quote! { #import::<_> } + } + }; + + // full: &["name", "alias", ...] or &[] + let full_value: TS2 = if !full_names.is_empty() { + let strs: Vec<proc_macro2::Literal> = full_names + .iter() + .map(|s| proc_macro2::Literal::string(s)) + .collect(); + quote! { &[#(#strs),*] } + } else { + quote! { &[] } + }; + + // short: Some('c') or None + let short_value: TS2 = match short_char { + Some(c) => { + let lit = proc_macro2::Literal::character(c); + quote! { ::std::option::Option::Some(#lit) } + } + None => quote! { ::std::option::Option::None }, + }; + + let positional_value: bool = !is_named; + + let result = quote! { + #path { + full: #full_value, + short: #short_value, + positional: #positional_value, + internal_type: ::std::marker::PhantomData, + } + }; + + result.into() +} + +fn split_at_commas(tokens: &[TokenTree]) -> Vec<Vec<TokenTree>> { + let mut result = vec![Vec::new()]; + let mut depth = 0u32; + for t in tokens { + match t { + TokenTree::Group(_g) => { + depth += 1; + result.last_mut().unwrap().push(t.clone()); + depth -= 1; + } + TokenTree::Punct(p) if p.as_char() == ',' && depth == 0 => { + result.push(Vec::new()); + } + _ => result.last_mut().unwrap().push(t.clone()), + } + } + result +} + +fn is_double_colon(tokens: &[TokenTree], pos: usize) -> bool { + pos > 0 && matches!(&tokens[pos - 1], TokenTree::Punct(p) if p.as_char() == ':') +} + +fn is_char_literal(tokens: &[TokenTree]) -> bool { + tokens.len() == 1 + && matches!(&tokens[0], TokenTree::Literal(l) if { + let s = l.to_string(); + s.starts_with('\'') && s.len() >= 3 + }) +} + +fn is_string_literal(tokens: &[TokenTree]) -> bool { + tokens.len() == 1 + && matches!(&tokens[0], TokenTree::Literal(l) if { + l.to_string().starts_with('"') + }) +} + +fn extract_char(tokens: &[TokenTree]) -> Option<char> { + match &tokens[0] { + TokenTree::Literal(l) => { + let s = l.to_string(); + let cs: Vec<char> = s.chars().collect(); + if cs.len() >= 3 && cs[0] == '\'' && cs[cs.len() - 1] == '\'' { + let inner: String = cs[1..cs.len() - 1].iter().collect(); + // inner is the character between the quotes of a char literal. + // For a literal `'n'`, inner is `"n"` (the character n). + // For an escape `'\n'`, inner is the actual newline character. + // The catch-all handles both literal single chars and escape + // sequences (the escaped char IS the actual control character). + match inner.as_str() { + "\\\\" => Some('\\'), + "\\'" => Some('\''), + _ => inner.chars().next(), + } + } else { + None + } + } + _ => None, + } +} + +fn extract_string(tokens: &[TokenTree]) -> Option<String> { + match &tokens[0] { + TokenTree::Literal(l) => { + let s = l.to_string(); + let cs: Vec<char> = s.chars().collect(); + if cs.len() >= 2 && cs[0] == '"' && cs[cs.len() - 1] == '"' { + Some(cs[1..cs.len() - 1].iter().collect()) + } else { + None + } + } + _ => None, + } +} + +fn join_idents(tokens: &[TokenTree]) -> String { + tokens + .iter() + .map(|t| match t { + TokenTree::Ident(id) => id.to_string(), + _ => String::new(), + }) + .collect() +} diff --git a/mingling_picker_macros/src/internal_repeat.rs b/mingling_picker_macros/src/internal_repeat.rs new file mode 100644 index 0000000..4b2242b --- /dev/null +++ b/mingling_picker_macros/src/internal_repeat.rs @@ -0,0 +1,315 @@ +use proc_macro::{Delimiter, Group, Ident, Literal, TokenStream, TokenTree}; + +pub(crate) fn internal_repeat(input: TokenStream) -> TokenStream { + let tokens: Vec<TokenTree> = input.into_iter().collect(); + let (range_start, range_end, body_start) = parse_range(&tokens); + + let mut body: Vec<TokenTree> = tokens[body_start..].to_vec(); + if body.len() == 1 + && let TokenTree::Group(g) = &body[0] + && g.delimiter() == Delimiter::Brace + { + body = g.stream().into_iter().collect(); + } + + let mut result = Vec::new(); + for i in range_start..=range_end { + result.extend(expand_body(&body, i, range_start, range_end)); + } + result.into_iter().collect() +} + +/// Parse `start .. end =>` or `start ..= end =>` or `count =>` (backward compat). +/// Returns `(start, end_inclusive, body_start_index)`. +fn parse_range(tokens: &[TokenTree]) -> (usize, usize, usize) { + // Find => separator + let arrow_pos = tokens.windows(2).position(|w| { + matches!(&w[0], TokenTree::Punct(p) if p.as_char() == '=') + && matches!(&w[1], TokenTree::Punct(p) if p.as_char() == '>') + }); + + let (arrow_pos, body_start) = match arrow_pos { + Some(p) => (p, p + 2), + None => return (1, 12, 0), // fallback + }; + + let before: Vec<&TokenTree> = tokens[..arrow_pos].iter().collect(); + + // Try to find `..` or `..=` pattern + // `..` is two Punct('.') tokens + let dotdot = before.windows(2).position(|w| { + matches!(w[0], TokenTree::Punct(p) if p.as_char() == '.') + && matches!(w[1], TokenTree::Punct(p) if p.as_char() == '.') + }); + + if let Some(dd) = dotdot { + // Start value: tokens before `..` + let start = parse_usize_tokens(&before[..dd]); + let after_dd = &before[dd + 2..]; + + // Check for `..=` (inclusive range) + let (inclusive, end_tokens) = if after_dd + .first() + .is_some_and(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == '=')) + { + (true, &after_dd[1..]) + } else { + (false, after_dd) + }; + + let end = parse_usize_tokens(end_tokens); + + if inclusive { + (start, end, body_start) + } else { + // Exclusive end: if end >= start, iterate start..end, so end_inclusive = end - 1 + if end > start { + (start, end - 1, body_start) + } else { + (1, 12, body_start) // fallback + } + } + } else { + // No `..` found — fallback to simple count + let count = parse_usize_tokens(&before); + (1, count, body_start) + } +} + +/// Parse a sequence of tokens as a single usize value. +fn parse_usize_tokens(tokens: &[&TokenTree]) -> usize { + let s: String = tokens + .iter() + .map(|t| match t { + TokenTree::Literal(l) => l.to_string(), + TokenTree::Ident(id) => id.to_string(), + _ => String::new(), + }) + .collect::<Vec<_>>() + .join("") + .replace(' ', ""); + + s.parse().unwrap_or(12) +} + +/// Walk tokens, replacing: +/// `$` → current +/// `^$` → max +/// `$^` → min +/// `$+` → current + 1 (clamped) +/// `$-` → current - 1 (clamped) +/// `ident$` → ident{current} +/// and expanding `( … )+` / `( … ,)+` / `( … ;)+` groups. +fn expand_body(tokens: &[TokenTree], current: usize, min: usize, max: usize) -> Vec<TokenTree> { + let mut out = Vec::new(); + let mut i = 0; + while i < tokens.len() { + // Check for a parenthesized repetition group: ( ... ) sep? + + if let Some(exp) = try_expand_paren_group(tokens, i, current, min, max) { + let (items, consumed) = exp; + out.extend(items); + i += consumed; + continue; + } + + // `^$` — max value + if let TokenTree::Punct(p) = &tokens[i] + && p.as_char() == '^' + && i + 1 < tokens.len() + && let TokenTree::Punct(p2) = &tokens[i + 1] + && p2.as_char() == '$' + { + out.push(TokenTree::Literal(Literal::usize_suffixed(max))); + i += 2; + continue; + } + + // `ident$` / `ident$+` / `ident$-` / `ident$^` + // → {ident}{current} / {ident}{current+1} / {ident}{current-1} / {ident}{min} + if let TokenTree::Ident(id) = &tokens[i] { + if i + 1 < tokens.len() + && let TokenTree::Punct(p) = &tokens[i + 1] + && p.as_char() == '$' + { + // Check for ident$+ / ident$- / ident$^ + if i + 2 < tokens.len() { + match &tokens[i + 2] { + TokenTree::Punct(p2) if p2.as_char() == '+' => { + // ident$+ → {ident}{current+1} + let name = format!("{}{}", id, current + 1); + out.push(TokenTree::Ident(Ident::new(&name, id.span()))); + i += 3; + continue; + } + TokenTree::Punct(p2) if p2.as_char() == '-' => { + // ident$- → {ident}{current-1} + let val = current.saturating_sub(1); + let name = format!("{}{}", id, val); + out.push(TokenTree::Ident(Ident::new(&name, id.span()))); + i += 3; + continue; + } + TokenTree::Punct(p2) if p2.as_char() == '^' => { + let name = format!("{}{}", id, min); + out.push(TokenTree::Ident(Ident::new(&name, id.span()))); + i += 3; + continue; + } + _ => {} + } + } + // ident$ alone → {ident}{current} + let name = format!("{}{}", id, current); + out.push(TokenTree::Ident(Ident::new(&name, id.span()))); + i += 2; + continue; + } + out.push(tokens[i].clone()); + i += 1; + continue; + } + + match &tokens[i] { + TokenTree::Punct(p) if p.as_char() == '$' => { + // lookahead for $^, $+, $- + if i + 1 < tokens.len() { + match &tokens[i + 1] { + TokenTree::Punct(p2) if p2.as_char() == '^' => { + // $^ → min + out.push(TokenTree::Literal(Literal::usize_suffixed(min))); + i += 2; + continue; + } + TokenTree::Punct(p2) if p2.as_char() == '+' => { + // $+ → current + 1 + out.push(TokenTree::Literal(Literal::usize_suffixed(current + 1))); + i += 2; + continue; + } + TokenTree::Punct(p2) if p2.as_char() == '-' => { + // $- → current - 1 + let val = current.saturating_sub(1); + out.push(TokenTree::Literal(Literal::usize_suffixed(val))); + i += 2; + continue; + } + _ => {} + } + } + // `$` alone → current + out.push(TokenTree::Literal(Literal::usize_suffixed(current))); + i += 1; + continue; + } + TokenTree::Group(g) => { + let inner = expand_body_vec(&g.stream(), current, min, max); + out.push(TokenTree::Group(Group::new( + g.delimiter(), + inner.into_iter().collect(), + ))); + } + other => out.push(other.clone()), + } + i += 1; + } + out +} + +fn expand_body_vec(stream: &TokenStream, current: usize, min: usize, max: usize) -> Vec<TokenTree> { + let v: Vec<TokenTree> = stream.clone().into_iter().collect(); + expand_body(&v, current, min, max) +} + +/// Try to expand a repetition group. +/// +/// New syntax (repetition marker `+` is the LAST token INSIDE the parens): +/// - `(group,+)` — repeat `*` times (current), `,` is the separator +/// - `(group,+)` — repeat `*` times, `,` is the separator +/// - `(group,+)` — repeat `*` times, `;` is the separator +/// - `(group +)` — repeat `*` times, no separator +/// - `(group,+)+` — repeat `*+1` times (with separator) +/// - `(group,+)--` — repeat `*-1` times (with separator) [not yet used] +/// +/// The `*` is the current counter value. An optional `+` or `-` immediately +/// after the closing paren shifts the repeat count up or down by one. +fn try_expand_paren_group( + tokens: &[TokenTree], + i: usize, + current: usize, + _min: usize, + _max: usize, +) -> Option<(Vec<TokenTree>, usize)> { + let group = match tokens.get(i)? { + TokenTree::Group(g) if g.delimiter() == Delimiter::Parenthesis => g, + _ => return None, + }; + + let stream: Vec<TokenTree> = group.stream().into_iter().collect(); + + // Repetition syntax: the LAST token inside the parens MUST be `+`. + // `(content,+)` — repeat * times, `,` separator + // `(content;+)` — repeat * times, `;` separator + // `(content,+)` — repeat * times, no separator + // `(content,+)+` — repeat *+1 times + // `(content,+)-` — repeat *-1 times + // An optional `+` / `-` right after `)` shifts the count by ±1. + // + // Any `+` tokens inside `content` are treated as regular Rust syntax + // (trait bounds, etc.) — the repetition marker is ONLY the final `+`. + + let last_is_plus = stream + .last() + .is_some_and(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == '+')); + if !last_is_plus { + return None; + } + + // Determine separator and inner content. + let (inner, sep_str): (Vec<TokenTree>, &str) = { + if stream.len() >= 2 { + let sep_idx = stream.len() - 2; + match &stream[sep_idx] { + TokenTree::Punct(p) if p.as_char() == ',' => { + // (...,content,+) — inner is everything before the last `,` + let content: Vec<TokenTree> = stream[..sep_idx].into(); + (content, ",") + } + TokenTree::Punct(p) if p.as_char() == ';' => { + let content: Vec<TokenTree> = stream[..sep_idx].into(); + (content, ";") + } + _ => { + // (content+) — no separator, the `+` is the only special token + let content: Vec<TokenTree> = stream[..stream.len() - 1].into(); + (content, "") + } + } + } else { + // (+) — bare repetition marker, empty content + (vec![], "") + } + }; + + // Handle modifier after `)` + let rest = &tokens[i + 1..]; + let modifier: isize = match rest.first() { + Some(TokenTree::Punct(p)) if p.as_char() == '+' => 1, + Some(TokenTree::Punct(p)) if p.as_char() == '-' => -1, + _ => 0, + }; + let consumed = if modifier != 0 { 2 } else { 1 }; + let repeat_count = (current as isize + modifier) as usize; + + let mut out = Vec::new(); + for n in 1..=repeat_count { + if n > 1 && !sep_str.is_empty() { + out.push(TokenTree::Punct(proc_macro::Punct::new( + sep_str.chars().next().unwrap(), + proc_macro::Spacing::Alone, + ))); + } + out.extend(expand_body(&inner, n, 1, repeat_count)); + } + + Some((out, consumed)) +} diff --git a/mingling_picker_macros/src/lib.rs b/mingling_picker_macros/src/lib.rs new file mode 100644 index 0000000..a12e3ed --- /dev/null +++ b/mingling_picker_macros/src/lib.rs @@ -0,0 +1,32 @@ +#![doc = include_str!("../README.md")] + +use proc_macro::TokenStream; + +mod arg; +mod internal_repeat; + +/// Core proc-macro: repeats a template body `count` times. +/// +/// Internal call signature: `internal_repeat!(count => { template })` +#[proc_macro] +pub fn internal_repeat(input: TokenStream) -> TokenStream { + internal_repeat::internal_repeat(input) +} + +/// Quick builder for `PickerArg`. +/// +/// # Syntax +/// +/// ```ignore +/// use mingling_picker_macros::flag; +/// +/// let basic = arg![name: String]; +/// let with_short_name = arg![name: String, 'n']; +/// let with_short_alias = arg![name: String, 'n', "alias"]; +/// let positional = arg![String]; +/// let positional_with_name = arg![String, 'n', "alias"]; +/// ``` +#[proc_macro] +pub fn arg(input: TokenStream) -> TokenStream { + arg::arg(input) +} diff --git a/mling/res/template_0.2/command/Cargo.toml b/mling/res/template_0.2/command/Cargo.toml.tmpl index e69de29..e69de29 100644 --- a/mling/res/template_0.2/command/Cargo.toml +++ b/mling/res/template_0.2/command/Cargo.toml.tmpl diff --git a/mling/res/template_0.2/flat/Cargo.toml b/mling/res/template_0.2/flat/Cargo.toml.tmpl index e69de29..e69de29 100644 --- a/mling/res/template_0.2/flat/Cargo.toml +++ b/mling/res/template_0.2/flat/Cargo.toml.tmpl diff --git a/mling/res/template_0.2/modularity/Cargo.toml b/mling/res/template_0.2/modularity/Cargo.toml.tmpl index e69de29..e69de29 100644 --- a/mling/res/template_0.2/modularity/Cargo.toml +++ b/mling/res/template_0.2/modularity/Cargo.toml.tmpl diff --git a/mling/src/cli.rs b/mling/src/cli.rs index 3000645..67d2ef0 100644 --- a/mling/src/cli.rs +++ b/mling/src/cli.rs @@ -8,13 +8,13 @@ use crate::{ }; use colored::Colorize; use mingling::{ - Groupped, Program, + Groupped, Program, RenderResult, hook::ProgramHook, - macros::{chain, help, pack, program_setup, r_println, renderer}, + macros::{chain, help, pack, program_setup, renderer}, res::ResExitCode, setup::{ExitCodeSetup, HelpFlagSetup, QuietFlagSetup, StructuralRendererSetup}, }; -use std::{env::current_dir, path::PathBuf, process::exit, str::FromStr}; +use std::{env::current_dir, io::Write, path::PathBuf, process::exit, str::FromStr}; pub fn run() { #[cfg(windows)] @@ -167,21 +167,29 @@ pub fn handle_error_dispatcher_not_found(err: ErrorDispatcherNotFound) -> Next { } #[renderer] -pub fn render_mling_help(_prev: ResultMlingHelp, ec: &mut ResExitCode) { - r_println!("{}", markdown(include_str!("helps/mling_help.txt"))); +pub fn render_mling_help(_prev: ResultMlingHelp, ec: &mut ResExitCode) -> RenderResult { + let mut result = RenderResult::default(); + writeln!(result, "{}", markdown(include_str!("helps/mling_help.txt"))).ok(); ec.exit_code = 0; + result } #[renderer] -pub fn render_unknown_command(prev: ResultUnknownCommand, ec: &mut ResExitCode) { - r_println!( +pub fn render_unknown_command(prev: ResultUnknownCommand, ec: &mut ResExitCode) -> RenderResult { + let mut result = RenderResult::default(); + writeln!( + result, "{}", eformat_cargo!("no such command: `{}`", prev.bright_yellow().bold()) - ); - r_println!(); - r_println!( + ) + .ok(); + writeln!(result).ok(); + writeln!( + result, "{}", hformat_cargo!("view all commands with `cargo help mling`") - ); + ) + .ok(); ec.exit_code = 101; + result } diff --git a/mling/src/errors/io_error.rs b/mling/src/errors/io_error.rs index 9f93ad7..ac503cc 100644 --- a/mling/src/errors/io_error.rs +++ b/mling/src/errors/io_error.rs @@ -1,4 +1,9 @@ -use mingling::{macros::{group, r_println, renderer}, res::ResExitCode}; +use mingling::{ + RenderResult, + macros::{group, renderer}, + res::ResExitCode, +}; +use std::io::Write as _; use crate::eformat_cargo; @@ -46,167 +51,174 @@ pub const EC_IO_ERR_OUT_OF_MEMORY: i32 = 1037; pub const EC_IO_ERR_OTHER: i32 = 1038; #[renderer] -pub fn render_error_io(err: ErrorIo, ec: &mut ResExitCode) { +pub fn render_error_io(err: ErrorIo, ec: &mut ResExitCode) -> RenderResult { + let mut result = RenderResult::default(); match err.kind() { std::io::ErrorKind::NotFound => { - r_println!("{}", eformat_cargo!("file or directory not found")); + writeln!(result, "{}", eformat_cargo!("file or directory not found")).ok(); ec.exit_code = EC_IO_ERR_NOT_FOUND; } std::io::ErrorKind::PermissionDenied => { - r_println!("{}", eformat_cargo!("permission denied")); + writeln!(result, "{}", eformat_cargo!("permission denied")).ok(); ec.exit_code = EC_IO_ERR_PERMISSION_DENIED; } std::io::ErrorKind::ConnectionRefused => { - r_println!("{}", eformat_cargo!("connection refused")); + writeln!(result, "{}", eformat_cargo!("connection refused")).ok(); ec.exit_code = EC_IO_ERR_CONNECTION_REFUSED; } std::io::ErrorKind::ConnectionReset => { - r_println!("{}", eformat_cargo!("connection reset")); + writeln!(result, "{}", eformat_cargo!("connection reset")).ok(); ec.exit_code = EC_IO_ERR_CONNECTION_RESET; } std::io::ErrorKind::HostUnreachable => { - r_println!("{}", eformat_cargo!("host unreachable")); + writeln!(result, "{}", eformat_cargo!("host unreachable")).ok(); ec.exit_code = EC_IO_ERR_HOST_UNREACHABLE; } std::io::ErrorKind::NetworkUnreachable => { - r_println!("{}", eformat_cargo!("network unreachable")); + writeln!(result, "{}", eformat_cargo!("network unreachable")).ok(); ec.exit_code = EC_IO_ERR_NETWORK_UNREACHABLE; } std::io::ErrorKind::ConnectionAborted => { - r_println!("{}", eformat_cargo!("connection aborted")); + writeln!(result, "{}", eformat_cargo!("connection aborted")).ok(); ec.exit_code = EC_IO_ERR_CONNECTION_ABORTED; } std::io::ErrorKind::NotConnected => { - r_println!("{}", eformat_cargo!("not connected")); + writeln!(result, "{}", eformat_cargo!("not connected")).ok(); ec.exit_code = EC_IO_ERR_NOT_CONNECTED; } std::io::ErrorKind::AddrInUse => { - r_println!("{}", eformat_cargo!("address in use")); + writeln!(result, "{}", eformat_cargo!("address in use")).ok(); ec.exit_code = EC_IO_ERR_ADDR_IN_USE; } std::io::ErrorKind::AddrNotAvailable => { - r_println!("{}", eformat_cargo!("address not available")); + writeln!(result, "{}", eformat_cargo!("address not available")).ok(); ec.exit_code = EC_IO_ERR_ADDR_NOT_AVAILABLE; } std::io::ErrorKind::NetworkDown => { - r_println!("{}", eformat_cargo!("network down")); + writeln!(result, "{}", eformat_cargo!("network down")).ok(); ec.exit_code = EC_IO_ERR_NETWORK_DOWN; } std::io::ErrorKind::BrokenPipe => { - r_println!("{}", eformat_cargo!("broken pipe")); + writeln!(result, "{}", eformat_cargo!("broken pipe")).ok(); ec.exit_code = EC_IO_ERR_BROKEN_PIPE; } std::io::ErrorKind::AlreadyExists => { - r_println!("{}", eformat_cargo!("file or directory already exists")); + writeln!( + result, + "{}", + eformat_cargo!("file or directory already exists") + ) + .ok(); ec.exit_code = EC_IO_ERR_ALREADY_EXISTS; } std::io::ErrorKind::WouldBlock => { - r_println!("{}", eformat_cargo!("operation would block")); + writeln!(result, "{}", eformat_cargo!("operation would block")).ok(); ec.exit_code = EC_IO_ERR_WOULD_BLOCK; } std::io::ErrorKind::NotADirectory => { - r_println!("{}", eformat_cargo!("not a directory")); + writeln!(result, "{}", eformat_cargo!("not a directory")).ok(); ec.exit_code = EC_IO_ERR_NOT_A_DIRECTORY; } std::io::ErrorKind::IsADirectory => { - r_println!("{}", eformat_cargo!("is a directory")); + writeln!(result, "{}", eformat_cargo!("is a directory")).ok(); ec.exit_code = EC_IO_ERR_IS_A_DIRECTORY; } std::io::ErrorKind::DirectoryNotEmpty => { - r_println!("{}", eformat_cargo!("directory not empty")); + writeln!(result, "{}", eformat_cargo!("directory not empty")).ok(); ec.exit_code = EC_IO_ERR_DIRECTORY_NOT_EMPTY; } std::io::ErrorKind::ReadOnlyFilesystem => { - r_println!("{}", eformat_cargo!("read-only filesystem")); + writeln!(result, "{}", eformat_cargo!("read-only filesystem")).ok(); ec.exit_code = EC_IO_ERR_READ_ONLY_FILESYSTEM; } std::io::ErrorKind::StaleNetworkFileHandle => { - r_println!("{}", eformat_cargo!("stale network file handle")); + writeln!(result, "{}", eformat_cargo!("stale network file handle")).ok(); ec.exit_code = EC_IO_ERR_STALE_NETWORK_FILE_HANDLE; } std::io::ErrorKind::InvalidInput => { - r_println!("{}", eformat_cargo!("invalid input")); + writeln!(result, "{}", eformat_cargo!("invalid input")).ok(); ec.exit_code = EC_IO_ERR_INVALID_INPUT; } std::io::ErrorKind::InvalidData => { - r_println!("{}", eformat_cargo!("invalid data")); + writeln!(result, "{}", eformat_cargo!("invalid data")).ok(); ec.exit_code = EC_IO_ERR_INVALID_DATA; } std::io::ErrorKind::TimedOut => { - r_println!("{}", eformat_cargo!("timed out")); + writeln!(result, "{}", eformat_cargo!("timed out")).ok(); ec.exit_code = EC_IO_ERR_TIMED_OUT; } std::io::ErrorKind::WriteZero => { - r_println!("{}", eformat_cargo!("write zero")); + writeln!(result, "{}", eformat_cargo!("write zero")).ok(); ec.exit_code = EC_IO_ERR_WRITE_ZERO; } std::io::ErrorKind::StorageFull => { - r_println!("{}", eformat_cargo!("storage full")); + writeln!(result, "{}", eformat_cargo!("storage full")).ok(); ec.exit_code = EC_IO_ERR_STORAGE_FULL; } std::io::ErrorKind::NotSeekable => { - r_println!("{}", eformat_cargo!("not seekable")); + writeln!(result, "{}", eformat_cargo!("not seekable")).ok(); ec.exit_code = EC_IO_ERR_NOT_SEEKABLE; } std::io::ErrorKind::QuotaExceeded => { - r_println!("{}", eformat_cargo!("quota exceeded")); + writeln!(result, "{}", eformat_cargo!("quota exceeded")).ok(); ec.exit_code = EC_IO_ERR_QUOTA_EXCEEDED; } std::io::ErrorKind::FileTooLarge => { - r_println!("{}", eformat_cargo!("file too large")); + writeln!(result, "{}", eformat_cargo!("file too large")).ok(); ec.exit_code = EC_IO_ERR_FILE_TOO_LARGE; } std::io::ErrorKind::ResourceBusy => { - r_println!("{}", eformat_cargo!("resource busy")); + writeln!(result, "{}", eformat_cargo!("resource busy")).ok(); ec.exit_code = EC_IO_ERR_RESOURCE_BUSY; } std::io::ErrorKind::ExecutableFileBusy => { - r_println!("{}", eformat_cargo!("executable file busy")); + writeln!(result, "{}", eformat_cargo!("executable file busy")).ok(); ec.exit_code = EC_IO_ERR_EXECUTABLE_FILE_BUSY; } std::io::ErrorKind::Deadlock => { - r_println!("{}", eformat_cargo!("deadlock")); + writeln!(result, "{}", eformat_cargo!("deadlock")).ok(); ec.exit_code = EC_IO_ERR_DEADLOCK; } std::io::ErrorKind::CrossesDevices => { - r_println!("{}", eformat_cargo!("crosses devices")); + writeln!(result, "{}", eformat_cargo!("crosses devices")).ok(); ec.exit_code = EC_IO_ERR_CROSSES_DEVICES; } std::io::ErrorKind::TooManyLinks => { - r_println!("{}", eformat_cargo!("too many links")); + writeln!(result, "{}", eformat_cargo!("too many links")).ok(); ec.exit_code = EC_IO_ERR_TOO_MANY_LINKS; } std::io::ErrorKind::InvalidFilename => { - r_println!("{}", eformat_cargo!("invalid filename")); + writeln!(result, "{}", eformat_cargo!("invalid filename")).ok(); ec.exit_code = EC_IO_ERR_INVALID_FILENAME; } std::io::ErrorKind::ArgumentListTooLong => { - r_println!("{}", eformat_cargo!("argument list too long")); + writeln!(result, "{}", eformat_cargo!("argument list too long")).ok(); ec.exit_code = EC_IO_ERR_ARGUMENT_LIST_TOO_LONG; } std::io::ErrorKind::Interrupted => { - r_println!("{}", eformat_cargo!("interrupted")); + writeln!(result, "{}", eformat_cargo!("interrupted")).ok(); ec.exit_code = EC_IO_ERR_INTERRUPTED; } std::io::ErrorKind::Unsupported => { - r_println!("{}", eformat_cargo!("unsupported")); + writeln!(result, "{}", eformat_cargo!("unsupported")).ok(); ec.exit_code = EC_IO_ERR_UNSUPPORTED; } std::io::ErrorKind::UnexpectedEof => { - r_println!("{}", eformat_cargo!("unexpected end of file")); + writeln!(result, "{}", eformat_cargo!("unexpected end of file")).ok(); ec.exit_code = EC_IO_ERR_UNEXPECTED_EOF; } std::io::ErrorKind::OutOfMemory => { - r_println!("{}", eformat_cargo!("out of memory")); + writeln!(result, "{}", eformat_cargo!("out of memory")).ok(); ec.exit_code = EC_IO_ERR_OUT_OF_MEMORY; } std::io::ErrorKind::Other => { - r_println!("{}", eformat_cargo!(err.to_string())); + writeln!(result, "{}", eformat_cargo!(err.to_string())).ok(); ec.exit_code = EC_IO_ERR_OTHER; } _ => { - r_println!("{}", eformat_cargo!(err.to_string())); + writeln!(result, "{}", eformat_cargo!(err.to_string())).ok(); ec.exit_code = EC_IO_ERR_OTHER; } } + result } diff --git a/mling/src/lib.rs b/mling/src/lib.rs index 1bf38a7..1de1228 100644 --- a/mling/src/lib.rs +++ b/mling/src/lib.rs @@ -1,7 +1,7 @@ #![allow(unused_imports)] use mingling::{ - macros::{chain, gen_program, pack, r_println, renderer}, + macros::{chain, gen_program, pack, renderer}, res::ResExitCode, }; diff --git a/mling/src/pkg_mgr/installer.rs b/mling/src/pkg_mgr/installer.rs index e69de29..8b13789 100644 --- a/mling/src/pkg_mgr/installer.rs +++ b/mling/src/pkg_mgr/installer.rs @@ -0,0 +1 @@ + diff --git a/mling/src/proj_mgr/checklist_reader.rs b/mling/src/proj_mgr/checklist_reader.rs index 6d115bb..558d303 100644 --- a/mling/src/proj_mgr/checklist_reader.rs +++ b/mling/src/proj_mgr/checklist_reader.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, path::Path, io}; +use std::{collections::HashMap, io, path::Path}; /// Reads and parses a `CHECKLIST.md` file, extracting both *values* (from /// fenced code blocks) and *toggles* (from checkbox lines). @@ -56,27 +56,28 @@ impl CheckListReader { let line = lines[i]; if let Some(key) = line.strip_prefix("```").map(|s| s.trim()) - && !key.is_empty() && !key.starts_with(' ') { - let mut block_lines = Vec::new(); + && !key.is_empty() + && !key.starts_with(' ') + { + let mut block_lines = Vec::new(); + i += 1; + while i < lines.len() && !lines[i].trim_start().starts_with("```") { + block_lines.push(lines[i]); i += 1; - while i < lines.len() && !lines[i].trim_start().starts_with("```") { - block_lines.push(lines[i]); - i += 1; - } - let value = block_lines - .into_iter() - .find(|l| !l.trim().is_empty()) - .map(|l| l.trim().to_string()); - if let Some(val) = value { - self.values.insert(key.to_string(), val); - } - continue; } + let value = block_lines + .into_iter() + .find(|l| !l.trim().is_empty()) + .map(|l| l.trim().to_string()); + if let Some(val) = value { + self.values.insert(key.to_string(), val); + } + continue; + } if let Some(toggle_key) = Self::parse_toggle_line(line) { let is_checked = line.contains("[x]") || line.contains("[X]"); - self.all_toggles - .insert(toggle_key.clone(), is_checked); + self.all_toggles.insert(toggle_key.clone(), is_checked); if is_checked { self.toggles.insert(toggle_key, true); } diff --git a/mling/src/proj_mgr/generator.rs b/mling/src/proj_mgr/generator.rs index 48a1753..4f965d9 100644 --- a/mling/src/proj_mgr/generator.rs +++ b/mling/src/proj_mgr/generator.rs @@ -1,9 +1,10 @@ use std::path::{self, PathBuf}; use mingling::{ - Groupped, - macros::{chain, pack, r_println, renderer, route}, + Groupped, RenderResult, + macros::{chain, pack, renderer, route}, }; +use std::io::Write as _; use crate::{Next, proj_mgr::EntryGenerateProject, res::ResCurrentDir}; @@ -35,12 +36,22 @@ pub fn handle_state_gen_proj_ready(prev: StateGenerateProjectReady) -> Next { } #[renderer] -pub fn render_gen_proj_checklist_created(result: ResultGenerateProjectChecklistCreated) { - r_println!( +pub fn render_gen_proj_checklist_created( + result: ResultGenerateProjectChecklistCreated, +) -> RenderResult { + let mut res = RenderResult::default(); + writeln!( + res, "Successfully create {} at \"{}\"", CHECK_LIST_NAME, result.to_string_lossy() - ); - r_println!(""); - r_println!("Please fill in {CHECK_LIST_NAME} and run `mling gen` again to continue generating"); + ) + .ok(); + writeln!(res).ok(); + writeln!( + res, + "Please fill in {CHECK_LIST_NAME} and run `mling gen` again to continue generating" + ) + .ok(); + res } diff --git a/mling/src/proj_mgr/show_binaries.rs b/mling/src/proj_mgr/show_binaries.rs index d6872cf..9d5caf0 100644 --- a/mling/src/proj_mgr/show_binaries.rs +++ b/mling/src/proj_mgr/show_binaries.rs @@ -2,10 +2,11 @@ use std::path::PathBuf; use colored::Colorize; use mingling::{ - Groupped, - macros::{chain, pack, r_print, r_println, renderer}, + Groupped, RenderResult, + macros::{chain, pack, renderer}, }; use serde::Serialize; +use std::io::Write as _; use crate::{ Next, @@ -58,16 +59,20 @@ pub fn handle_show_binaries(_args: EntryShowBinaries, manifest_path: &ResManifes } #[renderer] -pub fn render_binaries(binaries: ResultBinaries) -> String { - r_println!("{}", "Binaries:".bright_cyan().bold()); +pub fn render_binaries(binaries: ResultBinaries) -> RenderResult { + let mut result = RenderResult::default(); + writeln!(result, "{}", "Binaries:".bright_cyan().bold()).ok(); if let Some(max_name_len) = binaries.binaries.iter().map(|b| b.name.len()).max() { for binary in &binaries.binaries { - r_println!( + writeln!( + result, " {:width$} `{}`", binary.name.bright_yellow().bold(), binary.path.display().to_string().italic(), width = max_name_len - ); + ) + .ok(); } } + result } diff --git a/mling/src/proj_mgr/show_directories.rs b/mling/src/proj_mgr/show_directories.rs index e84bf69..7d7c074 100644 --- a/mling/src/proj_mgr/show_directories.rs +++ b/mling/src/proj_mgr/show_directories.rs @@ -1,9 +1,10 @@ use colored::Colorize; use mingling::{ - Groupped, - macros::{chain, pack, r_println, renderer}, + Groupped, RenderResult, + macros::{chain, pack, renderer}, }; use serde::Serialize; +use std::io::Write as _; use crate::{ Next, @@ -46,11 +47,15 @@ pub fn handle_show_target_directory( } #[renderer] -pub fn render_workspace_directory(prev: ResultWorkspaceDirectory) { - r_println!("{}", prev.path.bright_cyan().bold()); +pub fn render_workspace_directory(prev: ResultWorkspaceDirectory) -> RenderResult { + let mut result = RenderResult::default(); + writeln!(result, "{}", prev.path.bright_cyan().bold()).ok(); + result } #[renderer] -pub fn render_target_directory(prev: ResultTargetDirectory) { - r_println!("{}", prev.path.bright_cyan().bold()); +pub fn render_target_directory(prev: ResultTargetDirectory) -> RenderResult { + let mut result = RenderResult::default(); + writeln!(result, "{}", prev.path.bright_cyan().bold()).ok(); + result } diff --git a/run-tools.ps1 b/run-tools.ps1 deleted file mode 100644 index 7aa2c9c..0000000 --- a/run-tools.ps1 +++ /dev/null @@ -1,60 +0,0 @@ -Set-Location -Path (Split-Path -Parent $MyInvocation.MyCommand.Path) -ErrorAction Stop - -# Collect all available tool names -$tools = @() - -if (Test-Path "dev_tools/scripts") { - $scripts = Get-ChildItem -Path "dev_tools/scripts/*.ps1", "dev_tools/scripts/*.py" - foreach ($script in $scripts) { - if ($script -is [System.IO.FileInfo]) { - $tools += $script.BaseName - } - } -} -if (Test-Path "dev_tools/src/bin") { - $files = Get-ChildItem -Path "dev_tools/src/bin/*.rs" - foreach ($file in $files) { - if ($file -is [System.IO.FileInfo]) { - $tools += $file.BaseName - } - } -} - -if ($args.Count -eq 0) { - Write-Host "Available:" - for ($i = 0; $i -lt $tools.Count; $i++) { - Write-Host (" [{0,2}] {1}" -f ($i + 1), $tools[$i]) - } - exit 1 -} - -$target_name = $args[0] - -# Check if input is a number -if ($target_name -match '^\d+$') { - $idx = [int]$target_name - 1 - if ($idx -ge 0 -and $idx -lt $tools.Count) { - $target_name = $tools[$idx] - } else { - Write-Host "Error: invalid number '$target_name', valid range is 1-$($tools.Count)" - exit 1 - } -} - -# Collect remaining arguments to pass to the script -$script_args = $args[1..$args.Count] - -$script_file_ps1 = "dev_tools/scripts/${target_name}.ps1" -$script_file_py = "dev_tools/scripts/${target_name}.py" -$rust_file = "dev_tools/src/bin/${target_name}.rs" - -if (Test-Path $script_file_ps1) { - & $script_file_ps1 $script_args -} elseif (Test-Path $script_file_py) { - python $script_file_py $script_args -} elseif (Test-Path $rust_file) { - cargo run --manifest-path dev_tools/Cargo.toml --bin $target_name --quiet -- $script_args -} else { - Write-Host "Error: target '$target_name' does not exist as a script or Rust program" - exit 1 -} diff --git a/run-tools.sh b/run-tools.sh deleted file mode 100755 index 9a68f0e..0000000 --- a/run-tools.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/bin/bash - -cd "$(dirname "$0")" || exit 1 - -# Collect all available tool names -tools=() - -if [ -d "dev_tools/scripts" ]; then - for file in dev_tools/scripts/*.sh; do - if [ -f "$file" ]; then - tools+=("$(basename "$file" .sh)") - fi - done - for file in dev_tools/scripts/*.py; do - if [ -f "$file" ]; then - tools+=("$(basename "$file" .py)") - fi - done -fi -if [ -d "dev_tools/src/bin" ]; then - for file in dev_tools/src/bin/*.rs; do - if [ -f "$file" ]; then - tools+=("$(basename "$file" .rs)") - fi - done -fi - -if [ $# -eq 0 ]; then - echo "Available:" - for i in "${!tools[@]}"; do - printf " [%2d] %s\n" $((i + 1)) "${tools[$i]}" - done - exit 1 -fi - -target_bin="$1" -shift # Remove the first argument (tool name), keep the rest as tool arguments - -# Check if input is a number -if [[ "$target_bin" =~ ^[0-9]+$ ]]; then - idx=$((target_bin - 1)) - if [ "$idx" -ge 0 ] && [ "$idx" -lt "${#tools[@]}" ]; then - target_bin="${tools[$idx]}" - else - echo "Error: invalid number '$target_bin', valid range is 1-${#tools[@]}" - exit 1 - fi -fi - -target_script="dev_tools/scripts/${target_bin}.sh" -target_python="dev_tools/scripts/${target_bin}.py" -target_file="dev_tools/src/bin/${target_bin}.rs" - -if [ -f "$target_script" ]; then - chmod +x "$target_script" - "$target_script" "$@" -elif [ -f "$target_python" ]; then - python "$target_python" "$@" -elif [ -f "$target_file" ]; then - cargo run --manifest-path dev_tools/Cargo.toml --bin "$target_bin" --quiet -- "$@" -else - echo "Error: target '$target_bin' does not exist" - exit 1 -fi @@ -0,0 +1,234 @@ +# +# ██ ██████ ██ ██ ███ ██ ██████ ███████ ██ +# ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ███ +# ██ ██████ ██ ██ ██ ██ ██ ██████ ███████ ██ +# ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +# ██ ██ ██ ██ ██████ ██ ████ ██ ██ ███████ ██ +# +# You can go to [https://catilgrass.github.io/run] to install it +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +# Version: 0.1.1 + +Set-Location -Path (Split-Path -Parent $MyInvocation.MyCommand.Path) -ErrorAction Stop + +$tools = @{} + +if (Test-Path ".run/src/bin/*.ps1") { + Get-ChildItem -Path ".run/src/bin/*.ps1" | ForEach-Object { + $tools[$_.BaseName] = @{ Type = "ps1"; Path = $_.FullName } + } +} + +if (Test-Path ".run/src/bin/*.cs") { + Get-ChildItem -Path ".run/src/bin/*.cs" | ForEach-Object { + $tools[$_.BaseName] = @{ Type = "cs"; Path = $_.FullName } + } +} + +if (Test-Path ".run/src/bin/*.exe") { + Get-ChildItem -Path ".run/src/bin/*.exe" | ForEach-Object { + $tools[$_.BaseName] = @{ Type = "exe"; Path = $_.FullName } + } +} + +if (Test-Path ".run/src/bin/*.go") { + Get-ChildItem -Path ".run/src/bin/*.go" | ForEach-Object { + $tools[$_.BaseName] = @{ Type = "go"; Path = $_.FullName } + } +} + +if (Test-Path ".run/src/bin/*.nim") { + Get-ChildItem -Path ".run/src/bin/*.nim" | ForEach-Object { + $tools[$_.BaseName] = @{ Type = "nim"; Path = $_.FullName } + } +} + +if (Test-Path ".run/src/bin/*.pl") { + Get-ChildItem -Path ".run/src/bin/*.pl" | ForEach-Object { + $tools[$_.BaseName] = @{ Type = "pl"; Path = $_.FullName } + } +} + +if (Test-Path ".run/src/bin/*.py") { + Get-ChildItem -Path ".run/src/bin/*.py" | ForEach-Object { + $tools[$_.BaseName] = @{ Type = "py"; Path = $_.FullName } + } +} + +if (Test-Path ".run/src/bin/*.rb") { + Get-ChildItem -Path ".run/src/bin/*.rb" | ForEach-Object { + $tools[$_.BaseName] = @{ Type = "rb"; Path = $_.FullName } + } +} + +if (Test-Path ".run/src/bin/*.rs") { + Get-ChildItem -Path ".run/src/bin/*.rs" | ForEach-Object { + $tools[$_.BaseName] = @{ Type = "rs"; Path = $_.FullName } + } +} + +if (Test-Path ".run/src/bin/*.zig") { + Get-ChildItem -Path ".run/src/bin/*.zig" | ForEach-Object { + $tools[$_.BaseName] = @{ Type = "zig"; Path = $_.FullName } + } +} + +if ($args.Count -eq 0) { + $sorted = @($tools.Keys | Sort-Object @{Expression={if ($_[0] -cmatch '[A-Z]') {0} else {1}}}, @{Expression={if ($tools[$_].Type -eq 'ps1') {0} else {1}}}, {$_}) + $total = $sorted.Count + $num_w = $total.ToString().Length + + $max_name = ($sorted | ForEach-Object { $_.Length } | Measure-Object -Maximum).Maximum + + $inner_w = 2 + $num_w + 1 + 1 + $max_name + 2 + 10 + 1 + 2 + if ($inner_w -lt 38) { $inner_w = 38 } + + $title = '.\run.ps1 <NUMBER/NAME> [ARGS...]' + $title_len = $title.Length + $dash_total = $inner_w - 2 - $title_len + $dash_left = [Math]::Floor($dash_total / 2) + $dash_right = $dash_total - $dash_left + + Write-Host ("┌" + ("─" * $dash_left) + " " + $title + " " + ("─" * $dash_right) + "┐") + Write-Host ("│" + (" " * $inner_w) + "│") + + $i = 1 + foreach ($name in $sorted) { + $type = $tools[$name].Type + $lang = switch ($type) { + "ps1" { "PowerShell" } + "cs" { "C#" } + "exe" { "Binary" } + "go" { "Go" } + "nim" { "Nim" } + "pl" { "Perl" } + "py" { "Python" } + "rb" { "Ruby" } + "rs" { "Rust" } + "zig" { "Zig" } + } + $displayName = $name -replace '[_\-]', ' ' + $entry = " " + $i.ToString().PadRight($num_w) + ") " + $displayName.PadRight($max_name) + " [$lang]" + Write-Host ("│" + $entry.PadRight($inner_w) + "│") + $i++ + } + + Write-Host ("│" + (" " * $inner_w) + "│") + Write-Host ("└" + ("─" * $inner_w) + "┘") + exit 1 +} + +$target_name = $args[0] + +if ($target_name -match '^\d+$') { + $sorted = @($tools.Keys | Sort-Object @{Expression={if ($_[0] -cmatch '[A-Z]') {0} else {1}}}, @{Expression={if ($tools[$_].Type -eq 'ps1') {0} else {1}}}, {$_}) + $idx = [int]$target_name - 1 + if ($idx -ge 0 -and $idx -lt $sorted.Count) { + $target_name = $sorted[$idx] + } else { + Write-Host "Error: invalid number '$target_name', valid range is 1-$($sorted.Count)" + exit 1 + } +} + +if (-not $tools.ContainsKey($target_name)) { + $normalized = $target_name.ToLower() -replace '[ _\-\.]', '' + $found = $null + foreach ($key in $tools.Keys) { + $keyNormalized = $key.ToLower() -replace '[ _\-\.]', '' + if ($normalized -eq $keyNormalized) { + $found = $key + break + } + } + if ($found) { + $target_name = $found + } else { + Write-Host "Error: target '$target_name' does not exist" + exit 1 + } +} + +$script_args = $args[1..$args.Count] +$info = $tools[$target_name] + +switch ($info.Type) { + "ps1" { + & $info.Path @script_args + } + "cs" { + $temp_dir = ".run/target/csproj/$target_name" + $null = New-Item -ItemType Directory -Path $temp_dir -Force + + $props_content = @' +<Project> + <PropertyGroup> + <BaseOutputPath>$(MSBuildThisFileDirectory)../../csharp/bin</BaseOutputPath> + <BaseIntermediateOutputPath>$(MSBuildThisFileDirectory)../../csharp/obj</BaseIntermediateOutputPath> + </PropertyGroup> +</Project> +'@ + Set-Content -Path "$temp_dir/Directory.Build.props" -Value $props_content + + $csproj_content = @" +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net8.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + </PropertyGroup> + +</Project> +"@ + Set-Content -Path "$temp_dir/$target_name.csproj" -Value $csproj_content + Copy-Item -Path $info.Path -Destination "$temp_dir/Program.cs" -Force + + dotnet run --project "$temp_dir/$target_name.csproj" -- $script_args + } + "exe" { + & $info.Path $script_args + } + "go" { + go run $info.Path $script_args + } + "nim" { + nim r --hints:off $info.Path $script_args + } + "pl" { + perl $info.Path $script_args + } + "py" { + python $info.Path $script_args + } + "rb" { + ruby $info.Path $script_args + } + "rs" { + if (-not (Test-Path ".run/Cargo.toml")) { +@" +[package] +name = "run_rust" +version = "0.1.0" +edition = "2024" + +[workspace] + +[dependencies] +"@ | Set-Content -Path ".run/Cargo.toml" + } + cargo build --manifest-path ".run/Cargo.toml" --target-dir ".run/target" --bin $target_name --quiet + $binary = ".run/target/debug/$target_name.exe" + if (-not (Test-Path $binary)) { + $binary = ".run/target/debug/$target_name" + } + & $binary $script_args + } + "zig" { + zig run $info.Path $script_args + } +} + +exit $LASTEXITCODE @@ -0,0 +1,273 @@ +#!/bin/bash + +# +# ██ ██████ ██ ██ ███ ██ ███████ ██ ██ +# ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ +# ██ ██████ ██ ██ ██ ██ ██ ███████ ███████ +# ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +# ██ ██ ██ ██ ██████ ██ ████ ██ ███████ ██ ██ +# +# You can go to [https://catilgrass.github.io/run] to install it +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +# Version: 0.1.1 + +cd "$(dirname "$0")" || exit 1 + +declare -A tools + +for file in .run/src/bin/*.sh; do + if [ -f "$file" ]; then + name=$(basename "$file" .sh) + tools["$name"]="sh" + fi +done + +for file in .run/src/bin/*; do + if [ -f "$file" ]; then + name=$(basename "$file") + if [[ ! "$name" == *.* ]]; then + tools["$name"]="binary" + fi + fi +done + +for file in .run/src/bin/*.cs; do + if [ -f "$file" ]; then + name=$(basename "$file" .cs) + tools["$name"]="cs" + fi +done + +for file in .run/src/bin/*.go; do + if [ -f "$file" ]; then + name=$(basename "$file" .go) + tools["$name"]="go" + fi +done + +for file in .run/src/bin/*.nim; do + if [ -f "$file" ]; then + name=$(basename "$file" .nim) + tools["$name"]="nim" + fi +done + +for file in .run/src/bin/*.pl; do + if [ -f "$file" ]; then + name=$(basename "$file" .pl) + tools["$name"]="pl" + fi +done + +for file in .run/src/bin/*.py; do + if [ -f "$file" ]; then + name=$(basename "$file" .py) + tools["$name"]="py" + fi +done + +for file in .run/src/bin/*.rb; do + if [ -f "$file" ]; then + name=$(basename "$file" .rb) + tools["$name"]="rb" + fi +done + +for file in .run/src/bin/*.rs; do + if [ -f "$file" ]; then + name=$(basename "$file" .rs) + tools["$name"]="rs" + fi +done + +for file in .run/src/bin/*.zig; do + if [ -f "$file" ]; then + name=$(basename "$file" .zig) + tools["$name"]="zig" + fi +done + +if [ $# -eq 0 ]; then + sorted_names=($( + for name in "${!tools[@]}"; do + first_char="${name:0:1}" + if [[ "$first_char" =~ [A-Z] ]]; then + case_pri="0" + else + case_pri="1" + fi + if [ "${tools[$name]}" = "sh" ]; then + lang_pri="0" + else + lang_pri="1" + fi + echo "$case_pri$lang_pri $name" + done | sort | while read -r _ n; do echo "$n"; done + )) + total=${#sorted_names[@]} + num_w=${#total} + + max_name=0 + for name in "${sorted_names[@]}"; do + len=${#name} + ((len > max_name)) && max_name=$len + done + + inner_w=$((2 + num_w + 1 + 1 + max_name + 2 + 6 + 1 + 2)) + ((inner_w < 38)) && inner_w=38 + + title="./run.sh <NUMBER/NAME> [ARGS...]" + title_len=${#title} + dash_total=$((inner_w - 2 - title_len)) + dash_left=$((dash_total / 2)) + dash_right=$((dash_total - dash_left)) + + echo "┌$(printf '─%.0s' $(seq 1 $dash_left)) $title $(printf '─%.0s' $(seq 1 $dash_right))┐" + printf "│%*s│\n" $inner_w "" + + i=1 + for name in "${sorted_names[@]}"; do + type="${tools[$name]}" + case "$type" in + sh) lang="Shell";; + binary) lang="Binary";; + cs) lang="C#";; + go) lang="Go";; + nim) lang="Nim";; + pl) lang="Perl";; + py) lang="Python";; + rb) lang="Ruby";; + rs) lang="Rust";; + zig) lang="Zig";; + esac + display_name=$(echo "$name" | tr '_-' ' ') + entry=$(printf " %-*d) %-*s [%s]" $num_w $i $max_name "$display_name" $lang) + printf "│%-*s│\n" $inner_w "$entry" + i=$((i + 1)) + done + + printf "│%*s│\n" $inner_w "" + echo "└$(printf '─%.0s' $(seq 1 $inner_w))┘" + exit 1 +fi + +target_name="$1" +shift + +if [[ "$target_name" =~ ^[0-9]+$ ]]; then + sorted=($( + for name in "${!tools[@]}"; do + first_char="${name:0:1}" + if [[ "$first_char" =~ [A-Z] ]]; then + case_pri="0" + else + case_pri="1" + fi + if [ "${tools[$name]}" = "sh" ]; then + lang_pri="0" + else + lang_pri="1" + fi + echo "$case_pri$lang_pri $name" + done | sort | while read -r _ n; do echo "$n"; done + )) + idx=$((target_name - 1)) + if [ "$idx" -ge 0 ] && [ "$idx" -lt "${#sorted[@]}" ]; then + target_name="${sorted[$idx]}" + else + echo "Error: invalid number '$target_name', valid range is 1-${#sorted[@]}" + exit 1 + fi +fi + +if [ -z "${tools[$target_name]}" ]; then + normalized_user=$(echo "$target_name" | tr '[:upper:]' '[:lower:]' | sed 's/[_. -]//g') + found="" + for existing_name in "${!tools[@]}"; do + normalized_existing=$(echo "$existing_name" | tr '[:upper:]' '[:lower:]' | sed 's/[_. -]//g') + if [ "$normalized_user" = "$normalized_existing" ]; then + found="$existing_name" + break + fi + done + if [ -n "$found" ]; then + target_name="$found" + else + echo "Error: target '$target_name' does not exist" + exit 1 + fi +fi + +type="${tools[$target_name]}" + +case "$type" in + sh) + chmod +x ".run/src/bin/$target_name.sh" + ".run/src/bin/$target_name.sh" "$@" + ;; + binary) + chmod +x ".run/src/bin/$target_name" + ".run/src/bin/$target_name" "$@" + ;; + cs) + temp_dir=".run/target/csproj/$target_name" + mkdir -p "$temp_dir" + cat > "$temp_dir/Directory.Build.props" <<'PROPS' +<Project> + <PropertyGroup> + <BaseOutputPath>$(MSBuildThisFileDirectory)../../csharp/bin</BaseOutputPath> + <BaseIntermediateOutputPath>$(MSBuildThisFileDirectory)../../csharp/obj</BaseIntermediateOutputPath> + </PropertyGroup> +</Project> +PROPS + cat > "$temp_dir/$target_name.csproj" <<'CSPROJ' +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net8.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + </PropertyGroup> + +</Project> +CSPROJ + cp ".run/src/bin/$target_name.cs" "$temp_dir/Program.cs" + dotnet run --project "$temp_dir/$target_name.csproj" -- "$@" + ;; + go) + go run ".run/src/bin/$target_name.go" "$@" + ;; + nim) + nim r --hints:off ".run/src/bin/$target_name.nim" "$@" + ;; + pl) + perl ".run/src/bin/$target_name.pl" "$@" + ;; + py) + python ".run/src/bin/$target_name.py" "$@" + ;; + rb) + ruby ".run/src/bin/$target_name.rb" "$@" + ;; + rs) + if [ ! -f ".run/Cargo.toml" ]; then + cat > ".run/Cargo.toml" <<'EOF' +[package] +name = "run_rust" +version = "0.1.0" +edition = "2024" + +[workspace] + +[dependencies] +EOF + fi + cargo build --manifest-path ".run/Cargo.toml" --target-dir ".run/target" --bin "$target_name" --quiet + ".run/target/debug/$target_name" "$@" + ;; + zig) + zig run ".run/src/bin/$target_name.zig" "$@" + ;; +esac diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..6360c18 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.95.0" +components = ["rustfmt", "clippy"] |
