diff options
149 files changed, 3447 insertions, 3774 deletions
diff --git a/.github/workflows/ci-check-only.yml b/.github/workflows/ci-check-only.yml index 1179ed0..2cff3a6 100644 --- a/.github/workflows/ci-check-only.yml +++ b/.github/workflows/ci-check-only.yml @@ -20,9 +20,9 @@ jobs: 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') }} + key: ${{ runner.os }}-mingling-temp-codes-${{ hashFiles('.run/Cargo.toml', '.run/src/**/*.rs', 'Cargo.lock', 'verified-docs.toml', 'examples/test-examples.toml') }} restore-keys: | - ${{ runner.os }}-mingling-temp- + ${{ runner.os }}-mingling-temp-codes- - run: cargo ci --test-codes @@ -40,8 +40,15 @@ jobs: 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') }} + key: ${{ runner.os }}-mingling-temp-docs-${{ hashFiles('.run/Cargo.toml', '.run/src/**/*.rs', 'Cargo.lock', 'verified-docs.toml', 'examples/test-examples.toml') }} restore-keys: | - ${{ runner.os }}-mingling-temp- + ${{ runner.os }}-mingling-temp-docs- - run: cargo ci --test-docs + + - name: Upload docs test result on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: docs-test-result-${{ matrix.os }}.md + path: .temp/DOCS-TEST-RESULT.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b81c1ef..0a6a9a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,9 +22,9 @@ jobs: 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') }} + key: ${{ runner.os }}-mingling-temp-codes-${{ hashFiles('.run/Cargo.toml', '.run/src/**/*.rs', 'Cargo.lock', 'verified-docs.toml', 'examples/test-examples.toml') }} restore-keys: | - ${{ runner.os }}-mingling-temp- + ${{ runner.os }}-mingling-temp-codes- - run: cargo ci --test-codes @@ -42,12 +42,19 @@ jobs: 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') }} + key: ${{ runner.os }}-mingling-temp-docs-${{ hashFiles('.run/Cargo.toml', '.run/src/**/*.rs', 'Cargo.lock', 'verified-docs.toml', 'examples/test-examples.toml') }} restore-keys: | - ${{ runner.os }}-mingling-temp- + ${{ runner.os }}-mingling-temp-docs- - run: cargo ci --test-docs + - name: Upload docs test result on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: docs-test-result-${{ matrix.os }}.md + path: .temp/DOCS-TEST-RESULT.md + Move-Unreleased-Tag: if: github.ref == 'refs/heads/main' needs: [Check-Codes, Check-Docs] @@ -78,6 +85,12 @@ jobs: - name: Build API docs run: cargo run --manifest-path .run/Cargo.toml --bin deploy-api-docs + - name: Install cargo-llvm-cov + run: cargo install cargo-llvm-cov + + - name: Run cov-test + run: cargo run --manifest-path .run/Cargo.toml --bin cov-test + - name: Setup Pages uses: actions/configure-pages@v5 - name: Upload artifact @@ -6,6 +6,9 @@ # Temp file for .run/src/bin/deploy-api-docs.rs docs/api-docs/ +# Temp file for .run/src/bin/cov-test.rs +docs/cov-test/ + # Drafts __*.md __*/ diff --git a/.run/.gitignore b/.run/.gitignore index 0dc39b0..40f407e 100644 --- a/.run/.gitignore +++ b/.run/.gitignore @@ -3,5 +3,3 @@ # If you want to add some Rust crates? Remove this line # /Cargo.* - -last_check diff --git a/.run/src/bin/ci.rs b/.run/src/bin/ci.rs index 2527ece..f3ae9e8 100644 --- a/.run/src/bin/ci.rs +++ b/.run/src/bin/ci.rs @@ -123,14 +123,26 @@ fn ci(test_docs: bool, test_codes: bool, run_all: bool) -> Result<(), i32> { } if run_all || test_docs { - println_cargo_style!("Phase: Test all examples"); - test_examples()?; + let mut exit_code = 0; println_cargo_style!("Phase: Verify all *.md document code blocks are compilable"); - test_docs_code_blocks()?; + if let Err(code) = test_docs_code_blocks() { + exit_code = exit_code.max(code); + } + + println_cargo_style!("Phase: Test all examples"); + if let Err(code) = test_examples() { + exit_code = exit_code.max(code); + } println_cargo_style!("Phase: Check all documentation is up to date"); - docs_refresh()?; + if let Err(code) = docs_refresh() { + exit_code = exit_code.max(code); + } + + if exit_code != 0 { + return Err(exit_code); + } } run_cmd!("git add --renormalize .")?; diff --git a/.run/src/bin/cov-test.rs b/.run/src/bin/cov-test.rs new file mode 100644 index 0000000..a53f74c --- /dev/null +++ b/.run/src/bin/cov-test.rs @@ -0,0 +1,106 @@ +use std::fs; +use tools::{println_cargo_style, run_cmd}; + +const OUTPUT_DIR: &str = "docs/cov-test"; + +fn main() { + let repo_root = find_git_repo().expect("Failed to find git repository root"); + let output_path = repo_root.join(OUTPUT_DIR); + + // Read features from [package.metadata.docs.rs] + let features = tools::read_features().unwrap_or_else(|e| { + eprintln!("Error: {}", e); + std::process::exit(1); + }); + + let features_arg = features.join(","); + + // Ensure output directory exists + std::fs::create_dir_all(&output_path).expect("Failed to create output directory"); + + let cmd = format!( + "cargo llvm-cov --html --output-dir \"{}\" --workspace --features \"{}\" --color always", + output_path.to_string_lossy(), + features_arg, + ); + + println_cargo_style!("Features: {}", features_arg); + println_cargo_style!("Coverage: {}", output_path.display()); + + println_cargo_style!("Running: cargo llvm-cov --html"); + run_cmd!(&cmd).unwrap_or_else(|code| { + eprintln!("Error: cargo llvm-cov failed with exit code {}", code); + std::process::exit(code); + }); + + // Move files from <output_path>/html/ to <output_path> + let html_dir = output_path.join("html"); + if html_dir.exists() && html_dir.is_dir() { + println_cargo_style!("Moving files from {}/html/ to {}/", OUTPUT_DIR, OUTPUT_DIR); + + // Move each entry in html_dir up one level + for entry in fs::read_dir(&html_dir).expect("Failed to read html directory") { + let entry = entry.expect("Failed to read entry"); + let entry_path = entry.path(); + let file_name = entry + .file_name() + .to_str() + .expect("Invalid filename") + .to_owned(); + + let dest_path = output_path.join(&file_name); + // Remove existing file/directory at destination if any + if dest_path.exists() { + if dest_path.is_dir() { + fs::remove_dir_all(&dest_path).unwrap_or_else(|e| { + eprintln!( + "Warning: could not remove directory {}: {}", + dest_path.display(), + e + ); + }); + } else { + fs::remove_file(&dest_path).unwrap_or_else(|e| { + eprintln!( + "Warning: could not remove file {}: {}", + dest_path.display(), + e + ); + }); + } + } + fs::rename(&entry_path, &dest_path).unwrap_or_else(|e| { + eprintln!("Warning: could not move {}: {}", entry_path.display(), e); + }); + } + + // Remove the now-empty html directory + fs::remove_dir(&html_dir).unwrap_or_else(|e| { + eprintln!("Warning: could not remove html directory: {}", e); + }); + + println_cargo_style!("Files moved successfully."); + } + + println_cargo_style!( + "Done: coverage report generated at {}/index.html", + OUTPUT_DIR + ); +} + +fn find_git_repo() -> Option<std::path::PathBuf> { + let mut current_dir = std::env::current_dir().ok()?; + + loop { + let git_dir = current_dir.join(".git"); + if git_dir.exists() && git_dir.is_dir() { + return Some(current_dir); + } + + if !current_dir.pop() { + break; + } + } + + None +} diff --git a/.run/src/bin/deploy-api-docs.rs b/.run/src/bin/deploy-api-docs.rs index ea52886..7aa093c 100644 --- a/.run/src/bin/deploy-api-docs.rs +++ b/.run/src/bin/deploy-api-docs.rs @@ -2,43 +2,16 @@ use std::path::Path; use tools::{println_cargo_style, run_cmd}; -const DOCS_RS_MANIFEST: &str = "mingling/Cargo.toml"; const OUTPUT_DIR: &str = "docs/api-docs"; fn main() { let repo_root = find_git_repo().expect("Failed to find git repository root"); - let manifest_path = repo_root.join(DOCS_RS_MANIFEST); - if !manifest_path.exists() { - eprintln!("Error: Manifest not found at {}", manifest_path.display()); + // Read features from [package.metadata.docs.rs] + let features = tools::read_features().unwrap_or_else(|e| { + eprintln!("Error: {}", e); std::process::exit(1); - } - - // Read and parse [package.metadata.docs.rs] - let manifest_content = - std::fs::read_to_string(&manifest_path).expect("Failed to read Cargo.toml"); - let cargo_toml: toml::Value = manifest_content - .parse() - .expect("Failed to parse Cargo.toml"); - - let doc_features = cargo_toml - .get("package") - .and_then(|p| p.get("metadata")) - .and_then(|m| m.get("docs")) - .and_then(|d| d.get("rs")) - .and_then(|rs| rs.get("features")) - .and_then(|f| f.as_array()) - .unwrap_or_else(|| { - eprintln!("Error: [package.metadata.docs.rs] or its 'features' key not found in mingling/Cargo.toml"); - std::process::exit(1); - }); - - let features: Vec<&str> = doc_features.iter().filter_map(|v| v.as_str()).collect(); - - if features.is_empty() { - eprintln!("Error: No features defined in [package.metadata.docs.rs]"); - std::process::exit(1); - } + }); let features_arg = features.join(","); diff --git a/.run/src/bin/doc.ps1 b/.run/src/bin/doc.ps1 index fd7afaa..0e55141 100644 --- a/.run/src/bin/doc.ps1 +++ b/.run/src/bin/doc.ps1 @@ -1 +1,5 @@ -cargo doc --workspace --no-deps --features core,macros,builds,structural_renderer,repl,comp,parser,picker,clap,extra_macros --open +cargo doc ` + --manifest-path mingling/Cargo.toml ` + --no-deps ` + --features docs_rs,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 index 2d59896..014ea44 100644..100755 --- a/.run/src/bin/doc.sh +++ b/.run/src/bin/doc.sh @@ -1,3 +1,7 @@ #!/bin/bash -cargo doc --workspace --no-deps --features core,macros,builds,structural_renderer,repl,comp,parser,picker,clap,extra_macros --open +cargo doc \ + --manifest-path mingling/Cargo.toml \ + --no-deps \ + --features docs_rs,core,macros,builds,structural_renderer,repl,comp,parser,picker,clap,extra_macros \ + --open diff --git a/.run/src/bin/http-page-preview.sh b/.run/src/bin/http-page-preview.sh index bed4b1c..bed4b1c 100644..100755 --- a/.run/src/bin/http-page-preview.sh +++ b/.run/src/bin/http-page-preview.sh diff --git a/.run/src/bin/test-all-markdown-code.rs b/.run/src/bin/test-all-markdown-code.rs index 280fca7..1c0c9e2 100644 --- a/.run/src/bin/test-all-markdown-code.rs +++ b/.run/src/bin/test-all-markdown-code.rs @@ -5,7 +5,7 @@ use std::path::{Path, PathBuf}; use colored::Colorize; use indicatif::ProgressBar; use tools::verify::{ - build_block, compute_block_hash, generate_cargo_toml, generate_main_rs, generate_build_rs, + build_block, compute_block_hash, generate_build_rs, generate_cargo_toml, generate_main_rs, is_block_testable, parse_code_blocks, write_summary_report, }; use tools::{eprintln_cargo_style, println_cargo_style}; @@ -13,16 +13,7 @@ use tools::{eprintln_cargo_style, println_cargo_style}; /// Config from verified-docs.toml #[derive(serde::Deserialize)] struct Config { - verified: VerifiedPaths, -} - -#[derive(serde::Deserialize)] -struct VerifiedPaths { - readme: String, - #[serde(default)] - documents_en_us: Option<String>, - #[serde(default)] - documents_zh_cn: Option<String>, + verified: HashMap<String, String>, } #[tokio::main] @@ -64,31 +55,28 @@ async fn main() { }; // Collect all markdown files from config + // Keys are used as labels; values are either single file paths or directory globs let mut files: Vec<(String, PathBuf)> = Vec::new(); - // README - let readme_path = PathBuf::from(&config.verified.readme); - if readme_path.exists() { - files.push(("README".to_string(), readme_path)); - } - - // English docs - if let Some(pattern) = &config.verified.documents_en_us { - let base = pattern.trim_end_matches("/**").trim_end_matches('*'); - let dir = PathBuf::from(base); - if dir.exists() && dir.is_dir() { - collect_md_files(&dir, &mut files, "en"); + for (key, value) in &config.verified { + let candidate = PathBuf::from(value); + if candidate.is_dir() { + // Directory — walk it for all .md files, using the key as label + collect_md_files(&candidate, &mut files, key); + } else if candidate.exists() && candidate.is_file() { + // Single file + files.push((key.to_string(), candidate)); + } else if candidate.extension().is_none() { + // No extension — treat as a glob like "docs/pages/**", walk the base dir instead + let base = PathBuf::from(value.trim_end_matches("/**").trim_end_matches('*')); + if base.is_dir() { + collect_md_files(&base, &mut files, key); + } } } - // Chinese docs - if let Some(pattern) = &config.verified.documents_zh_cn { - let base = pattern.trim_end_matches("/**").trim_end_matches('*'); - let dir = PathBuf::from(base); - if dir.exists() && dir.is_dir() { - collect_md_files(&dir, &mut files, "zh_CN"); - } - } + // Sort for deterministic ordering + files.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1))); // If a single file was specified, filter the list to only that file if let Some(ref target) = single_file { @@ -201,10 +189,7 @@ async fn main() { bar.inc(1); } else { bar.inc(1); - bar.println(format!( - " {} {block_label}", - "failed".bold().bright_red() - )); + bar.println(format!(" {} {block_label}", "failed".bold().bright_red())); bar.println(format!(" {block_label} FAILED:\n{err}")); } group_results.push((block.source_file.clone(), block.line, ok, err)); diff --git a/.run/src/bin/windows-folder-hide.ps1 b/.run/src/bin/windows-folder-hide.ps1 index 0ab2632..ff53202 100644 --- a/.run/src/bin/windows-folder-hide.ps1 +++ b/.run/src/bin/windows-folder-hide.ps1 @@ -1,43 +1,115 @@ -# Check `last_check` +$skipDirs = @('.git', '.temp', 'target', 'node_modules', '.pnpm') +$selfPath = (Get-Item -LiteralPath $MyInvocation.MyCommand.Path).Directory.FullName -$lastCheckFile = Join-Path $PSScriptRoot "last_check" -$currentTime = Get-Date -$timeThreshold = 10 +function Test-InSkipDir { + param( + [object]$Item + ) + $path = if ($Item -is [string]) { + $Item + } elseif ($Item.PSPath) { + $Item.PSPath -replace '^.*::', '' + } else { + $Item.FullName + } + + $parts = $path.Split([System.IO.Path]::DirectorySeparatorChar) + for ($i = 0; $i -lt $parts.Length - 1; $i++) { + if ($parts[$i] -in $skipDirs) { + return $true + } + } + return $false +} -if (Test-Path $lastCheckFile) { - $lastCheckTime = Get-Content $lastCheckFile | Get-Date - $timeDiff = ($currentTime - $lastCheckTime).TotalMinutes +function Invoke-UnhideRecursive { + param([string]$Path) + Get-ChildItem -LiteralPath $Path -Force | ForEach-Object { + if ($_.PSIsContainer) { + if ($_.Name -in $skipDirs) { + if ($_.Attributes -band [System.IO.FileAttributes]::Hidden) { + Write-Host " -> unhiding skip directory (self only): `"$($_.FullName)`"" + $_.Attributes = $_.Attributes -bxor [System.IO.FileAttributes]::Hidden + } + return + } + Invoke-UnhideRecursive $_.FullName + } else { + if ($_.Attributes -band [System.IO.FileAttributes]::Hidden) { + Write-Host " -> unhiding: `"$($_.FullName)`"" + $_.Attributes = $_.Attributes -bxor [System.IO.FileAttributes]::Hidden + } + } + } +} - if ($timeDiff -lt $timeThreshold) { - exit +function Test-GitPathSkippable { + param([string]$GitPath) + $parts = $GitPath.Split(@('/', '\')) + for ($i = 0; $i -lt $parts.Length - 1; $i++) { + if ($parts[$i] -in $skipDirs) { + return $true + } } + return $false } -$currentTime.ToString() | Out-File -FilePath $lastCheckFile -Force +Write-Host "Step 1: Unhiding all files and directories (skipping $($skipDirs -join ', '))..." -# Hide Files +Invoke-UnhideRecursive -Path (Get-Location).Path -Set-Location -Path (Join-Path $PSScriptRoot "..\..") +Write-Host "Step 2: Hiding git-ignored items..." -Get-ChildItem -Path . -Force -Recurse -ErrorAction SilentlyContinue | Where-Object { - $_.FullName -notmatch '\\.temp\\' -and $_.FullName -notmatch '\\.git\\' +git ls-files --others --ignored --exclude-standard | Where-Object { + -not (Test-GitPathSkippable $_) } | ForEach-Object { - attrib -h $_.FullName 2>&1 | Out-Null + $itemPath = $_ + Write-Host "... checking: `"$itemPath`"" + $item = Get-Item $_ -Force -ErrorAction SilentlyContinue + if (-not $item) { return } + + if ($item.FullName -eq $selfPath) { return } + + if (Test-InSkipDir $item) { + Write-Host " -> skipping (inside skip directory)" + return + } + + if ($item.PSIsContainer) { + if (-not ($item.Attributes -band [System.IO.FileAttributes]::Hidden)) { + Write-Host " -> hiding directory (non-recursive)" + $item.Attributes = $item.Attributes -bor [System.IO.FileAttributes]::Hidden + } + } else { + if (-not ($item.Attributes -band [System.IO.FileAttributes]::Hidden)) { + Write-Host " -> hiding" + $item.Attributes = $item.Attributes -bor [System.IO.FileAttributes]::Hidden + } + } } -Get-ChildItem -Path . -Force -Recurse -ErrorAction SilentlyContinue | Where-Object { - $_.Name -match '^\..*' -and $_.FullName -notmatch '\\\.\.$' -and $_.FullName -notmatch '\\\.$' -} | ForEach-Object { - attrib +h $_.FullName 2>&1 | Out-Null +Write-Host "Step 3: Hiding dot-prefixed items..." +Get-ChildItem -Path . -Force -Directory | Where-Object { $_.Name -match '^\.' } | ForEach-Object { + Write-Host "... checking: `"$($_.FullName)`"" + if (Test-InSkipDir $_) { + Write-Host " -> skipping (inside skip directory)" + return + } + if (-not ($_.Attributes -band [System.IO.FileAttributes]::Hidden)) { + Write-Host " -> hiding directory" + $_.Attributes = $_.Attributes -bor [System.IO.FileAttributes]::Hidden + } } -if (Get-Command git -ErrorAction SilentlyContinue) { - git status --ignored --short | ForEach-Object { - if ($_ -match '^!!\s+(.+)$') { - $ignoredPath = $matches[1] - if ($ignoredPath -notmatch '\.lnk$' -and (Test-Path $ignoredPath)) { - attrib +h $ignoredPath 2>&1 | Out-Null - } - } +Get-ChildItem -Path . -Force -File | Where-Object { $_.Name -match '^\.' } | ForEach-Object { + if ($_.FullName -eq $selfPath) { return } + Write-Host "... checking: `"$($_.FullName)`"" + if (Test-InSkipDir $_) { + Write-Host " -> skipping (inside skip directory)" + return + } + if (-not ($_.Attributes -band [System.IO.FileAttributes]::Hidden)) { + Write-Host " -> hiding file" + $_.Attributes = $_.Attributes -bor [System.IO.FileAttributes]::Hidden } } diff --git a/.run/src/lib.rs b/.run/src/lib.rs index d52c7fd..cff606a 100644 --- a/.run/src/lib.rs +++ b/.run/src/lib.rs @@ -334,6 +334,82 @@ pub fn run_cmd_with_progress(phase: &str, label: &str, cmd: String) -> Result<() } } +/// Read `[package.metadata.docs.rs].features` from `mingling/Cargo.toml`. +/// +/// Finds the git repository root, reads `mingling/Cargo.toml`, parses it as TOML, +/// and extracts the feature list under `[package.metadata.docs.rs].features`. +/// +/// # Errors +/// +/// Returns `std::io::Error` if: +/// - The git repository root cannot be found. +/// - The manifest file cannot be read. +/// - The TOML cannot be parsed. +/// - The `[package.metadata.docs.rs].features` key is missing or empty. +pub fn read_features() -> Result<Vec<String>, std::io::Error> { + // Find git repo root + let mut current_dir = std::env::current_dir()?; + let repo_root = loop { + let git_dir = current_dir.join(".git"); + if git_dir.exists() && git_dir.is_dir() { + break Some(current_dir); + } + if !current_dir.pop() { + break None; + } + }; + let repo_root = repo_root.ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::NotFound, + "Failed to find git repository root", + ) + })?; + + let manifest_path = repo_root.join("mingling/Cargo.toml"); + if !manifest_path.exists() { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("Manifest not found at {}", manifest_path.display()), + )); + } + + let manifest_content = std::fs::read_to_string(&manifest_path)?; + let cargo_toml: toml::Value = manifest_content.parse().map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Failed to parse Cargo.toml: {}", e), + ) + })?; + + let doc_features = cargo_toml + .get("package") + .and_then(|p| p.get("metadata")) + .and_then(|m| m.get("docs")) + .and_then(|d| d.get("rs")) + .and_then(|rs| rs.get("features")) + .and_then(|f| f.as_array()) + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::NotFound, + "[package.metadata.docs.rs] or its 'features' key not found in mingling/Cargo.toml", + ) + })?; + + let features: Vec<String> = doc_features + .iter() + .filter_map(|v| v.as_str().map(String::from)) + .collect(); + + if features.is_empty() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "No features defined in [package.metadata.docs.rs]", + )); + } + + Ok(features) +} + #[must_use] pub fn cargo_tomls() -> Vec<std::path::PathBuf> { let mut cargo_tomls = Vec::new(); diff --git a/.vscode/settings.json b/.vscode/settings.json index 1b585d1..11145a1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,18 +1,20 @@ { - "rust-analyzer.check.command": "clippy", - "rust-analyzer.checkOnSave": true, - "rust-analyzer.linkedProjects": [ - ".run/Cargo.toml", - "mingling_pathf/test/Cargo.toml", - "arg_picker/Cargo.toml", - "arg_picker/test/Cargo.toml", - ], - "rust-analyzer.cargo.features": [ - "structural_renderer", - "mingling_support", - "all_serde_fmt", - "picker", - "parser", - "comp", - ], + "rust-analyzer.check.command": "clippy", + "rust-analyzer.checkOnSave": true, + "rust-analyzer.linkedProjects": [ + ".run/Cargo.toml", + "mingling_pathf/test/Cargo.toml", + "mingling_cli/Cargo.toml", + "arg_picker/Cargo.toml", + "arg_picker/test/Cargo.toml" + ], + "rust-analyzer.cargo.features": [ + "structural_renderer", + "mingling_support", + "all_serde_fmt", + "docs_rs", + "picker", + "parser", + "comp" + ] } diff --git a/.zed/settings.json b/.zed/settings.json index 727f955..3fa2cba 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -1,26 +1,28 @@ { - "lsp": { - "rust-analyzer": { - "initialization_options": { - "checkOnSave": true, - "check": { "command": "clippy" }, - "linkedProjects": [ - ".run/Cargo.toml", - "mingling_pathf/test/Cargo.toml", - "arg_picker/Cargo.toml", - "arg_picker/test/Cargo.toml", - ], - "cargo": { - "features": [ - "structural_renderer", - "mingling_support", - "all_serde_fmt", - "picker", - "parser", - "comp", - ], - }, - }, - }, - }, + "lsp": { + "rust-analyzer": { + "initialization_options": { + "checkOnSave": true, + "check": { "command": "clippy" }, + "linkedProjects": [ + ".run/Cargo.toml", + "mingling_pathf/test/Cargo.toml", + "mingling_cli/Cargo.toml", + "arg_picker/Cargo.toml", + "arg_picker/test/Cargo.toml" + ], + "cargo": { + "features": [ + "structural_renderer", + "mingling_support", + "all_serde_fmt", + "docs_rs", + "picker", + "parser", + "comp" + ] + } + } + } + } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 997eac0..b90536e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,29 @@ None The `Routable` trait is defined in `mingling_core::asset::routable` and provides unified routing capabilities (`to_chain` / `to_render`) for any type that can be dispatched into the program's pipeline. A blanket implementation is provided for all `T: Grouped<C> + Send`, ensuring backward compatibility — existing types that implement `Grouped` automatically implement `Routable`. +2. **[`macros`]** Restructured the `mingling_macros` crate's internal module hierarchy. The previously flat module structure has been reorganized into a logical directory-based layout: + + - **`attr/`** — Attribute macro implementations (e.g., `#[chain]`, `#[renderer]`, `#[help]`, `#[completion]`, `#[dispatcher_clap]`, `#[program_setup]`) + - **`derive/`** — Derive macro implementations (e.g., `#[derive(Grouped)]`, `#[derive(EnumTag)]`) + - **`func/`** — Function-like macro implementations (e.g., `pack!`, `group!`, `dispatcher!`, `suggest!`, `entry!`, `node!`, `gen_program!` and its sub-macros) + - **`systems/`** — Cross-cutting systems (e.g., resource injection, dispatch tree generation, structural data derive support) + - **`extensions/`** — Extension point mechanism for attribute macros (unchanged) + - **`utils.rs`** — Shared utility module for future common helpers + + All public API items (`#[proc_macro]`, `#[proc_macro_derive]`, `#[proc_macro_attribute]`) remain at the crate root (`lib.rs`) with identical signatures. Internal function visibility has been tightened from `pub fn` to `pub(crate) fn` for all module-internal functions that were previously publicly accessible only within the crate. + + _No migration is required for downstream code — all macros are re-exported with the same names and signatures as before._ + +3. **[`macros`]** Refactored the code generation strategy for `#[chain]`, `#[renderer]`, `#[help]`, and `#[completion]` attribute macros. Instead of inlining the user's function body directly into the generated trait implementation, these macros now **preserve the original user function as a standalone item** and generate trait method implementations that **call the original function by name**, injecting resources from the application context. + + This approach provides several benefits: + + - **Better debugging and error messages** — The user's function exists as a named, callable item. Stack traces, profiler output, and error messages reference the user's function name (e.g., `handle_greet`) rather than an anonymous closure or inlined block, making debugging more intuitive. + + - **Clearer macro expansion** — The generated code maintains a clear separation between the original user function and the trait glue code, reducing the cognitive load when inspecting macro-expanded output. + + _No migration is required for downstream code — the behavior of all four macros is unchanged. This is purely an internal code generation refactoring._ + #### 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!`. @@ -217,24 +240,69 @@ None The return type validation has been removed entirely — any valid Rust return type is accepted. If the type does not implement `Into<ChainProcess<ThisProgram>>`, a standard Rust compilation error will be produced at the call site. +10. **[`macros`]** **[`extensions`]** Added the `extensions` module to `mingling_macros`, providing an extension point mechanism for attribute macros (`#[chain]`, `#[renderer]`, `#[help]`, `#[completion]`). The extension system allows identifiers in the attribute argument to be extracted and processed before the main macro logic runs. + + Each attribute macro now attempts to redispatch through `extensions::try_redispatch_simple()` (or `try_redispatch_completion` for `#[completion]`) before executing its standard logic. If extension identifiers are detected, the call is re-routed so that extensions are applied via additional `#[...]` attributes stacked on top of the inner core attribute. New extensions can be added without modifying the attribute macros themselves — only the `extensions` module needs to be updated to register new identifiers. + + This system is designed for future extensibility: as new cross-cutting concerns (e.g., logging, metrics, validation) are identified, they can be added as simple extension identifiers without touching the core macro logic. + +11. **[`extensions`]** **[`macros`]** Added the `#[routeify]` extension attribute macro that transforms `expr?` into `route!(expr)`, enabling concise error routing in chain functions using the `?` operator syntax. + + The `#[routeify]` macro can be used: + - **Standalone** — as a direct attribute: `#[routeify] fn handle(...) { ... }` + - **As an extension** — via the extension point system: `#[chain(routeify)] fn handle(...) { ... }` + + When used as a `#[chain]` extension, the `routeify` identifier is detected by the extension point mechanism, stripped from the `#[chain]` attribute arguments, and `#[routeify]` is applied as an outer attribute on top of `#[chain]`. The re-dispatch token stream now correctly generates `#[#exts]*` (i.e., `#[routeify] #[chain]`) instead of the previous bare `#exts` — fixing a bug where extension identifiers were emitted without the `#[...]` attribute delimiter, producing invalid token streams. + + ```rust + use mingling::macros::routeify; + + #[chain(routeify)] + fn handle_calc(args: EntryCalculate) -> Next { + let a = args.pick(&arg![f32]).to_result()?; + let op = args.pick(&arg![Operator]).to_result()?; + StateCalculate { number_a: a, operator: op, ... }.to_chain() + } + ``` + + The `#[routeify]` macro is feature-gated behind `extra_macros` and re-exported as `mingling::macros::routeify`. + + Internal changes: + - Added `mingling_macros/src/extensions/routeify.rs` with `routeify_impl` implementation. + - Updated `try_redispatch_simple` and `try_redispatch_completion` to emit `#[#exts]` instead of bare `#exts`, ensuring proper attribute syntax in the re-dispatched token stream. + - Registered `#[proc_macro_attribute] pub fn routeify` in `mingling_macros/src/lib.rs`. + #### **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. +1. **[`macros:renderer`]** **[`macros:help`]** Removed `r_println!` and `r_print!` macros from being implicitly injected by `#[renderer]` and `#[help]` macros. These macros still exist, but must now be used **explicitly** — either with an explicit buffer argument, or via the `#[buffer]` extension attribute that re-enables implicit buffer injection. - Renderers and help functions must now explicitly create and return a `RenderResult`: + **Option A — Explicit buffer:** Pass a `RenderResult` variable as the first argument: ```rust + use mingling::macros::r_println; use mingling::prelude::*; #[renderer] fn render_greeting(greeting: ResultGreeting) -> RenderResult { let mut result = RenderResult::new(); - writeln!(result, "Hello, {}!", *greeting).ok(); + r_println!(result, "Hello, {}!", *greeting); 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. + **Option B — Implicit buffer via `#[buffer]` extension:** Use `#[renderer(buffer)]` to re-enable the old implicit injection behavior, where a `RenderResult` buffer is automatically created and `r_println!`/`r_print!` write to it without an explicit argument. This requires the function to return `()` (unit); the expanded function will return `RenderResult`: + + ```rust + use mingling::macros::r_println; + + #[renderer(buffer)] + fn render_greeting(greeting: ResultGreeting) { + r_println!("Hello, {}!", *greeting); + // Returns RenderResult implicitly + } + ``` + + The `#[buffer]` extension is also available standalone as `#[buffer]` for use outside of `#[renderer]`/`#[help]` functions. 2. **[`macros:chain`]** The `#[chain]` macro's return type requirement has been relaxed. Previously, chain functions were required to return `Next` or `()` (with `()` auto-converting to `ResultEmpty`). Now, chain functions can also return `ChainProcess<ThisProgram>` directly, or omit the return type entirely (which defaults to `()` → `ResultEmpty`). @@ -280,6 +348,25 @@ None This is a pure rename — no behavioral changes. All functionality remains identical. Downstream code using the old `Groupped` name must migrate to `Grouped`. +5. **[`core`]** **[`macros`]** Removed `to_chain()` and `to_render()` default methods from the `Grouped` trait. These methods are now exclusively provided by the `Routable` trait. All code that previously called `to_chain()` or `to_render()` via `Grouped` must now call them via `Routable`: + + ```rust + // Before (via Grouped — removed) + use mingling::Grouped; + my_value.to_chain(); + + // After (via Routable) + use mingling::Routable; + my_value.to_chain(); + ``` + + - The `Routable` trait is re-exported in `mingling::prelude` alongside `Grouped`. + - The blanket implementation `impl<T: Grouped<C> + Send> Routable<C> for T` remains, so all types that implement `Grouped` still have `to_chain()` and `to_render()` — they just need to import `Routable` instead of relying on `Grouped` for those methods. + - Internal macro-generated code (in `#[chain]`, `#[renderer]`, `#[dispatcher_clap]`, `gen_program!`, `empty_result!`, etc.) has been updated to reference `::mingling::Routable::<C>::to_chain(...)` / `::mingling::Routable::<C>::to_render(...)` instead of `::mingling::Grouped::<C>::to_chain(...)` / `::mingling::Grouped::<C>::to_render(...)`. + - Downstream crates using `mingling` macros are automatically migrated — the macro output now references `Routable`. Only manual `.to_chain()` / `.to_render()` calls in user code need updating (add `use mingling::Routable;`). + + _No behavioral changes — this is a pure API migration from `Grouped` to `Routable` for routing methods._ + --- ## Release 0.2.2 (2026-07-10) @@ -12,15 +12,6 @@ dependencies = [ ] [[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] name = "anstream" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -88,12 +79,6 @@ dependencies = [ ] [[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] name = "bitflags" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -103,89 +88,24 @@ dependencies = [ ] [[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] -name = "cc" -version = "1.2.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] -name = "chrono" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link", -] - -[[package]] name = "colorchoice" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] -name = "colored" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys", -] - -[[package]] name = "env_filter" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -225,77 +145,12 @@ dependencies = [ ] [[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] name = "indexmap" version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -342,18 +197,6 @@ dependencies = [ ] [[package]] -name = "js-sys" -version = "0.3.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" -dependencies = [ - "cfg-if", - "futures-util", - "once_cell", - "wasm-bindgen", -] - -[[package]] name = "just_fmt" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -393,15 +236,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] -name = "libredox" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" -dependencies = [ - "libc", -] - -[[package]] name = "lock_api" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -436,20 +270,6 @@ dependencies = [ ] [[package]] -name = "mingling-cli" -version = "0.3.0" -dependencies = [ - "chrono", - "colored", - "dirs", - "just_fmt 0.1.2", - "mingling", - "serde", - "serde_json", - "toml", -] - -[[package]] name = "mingling_core" version = "0.3.0" dependencies = [ @@ -496,15 +316,6 @@ dependencies = [ ] [[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -517,12 +328,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] name = "parking_lot" version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -594,17 +399,6 @@ dependencies = [ ] [[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom", - "libredox", - "thiserror", -] - -[[package]] name = "regex" version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -648,12 +442,6 @@ dependencies = [ ] [[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] name = "ryu" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -731,12 +519,6 @@ dependencies = [ ] [[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] name = "signal-hook-registry" version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -753,12 +535,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b6709c7b6754dca1311b3c73e79fcce40dd414c782c66d88e8823030093b02b" [[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -786,26 +562,6 @@ dependencies = [ ] [[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] name = "tokio" version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -903,110 +659,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasm-bindgen" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -14,7 +14,7 @@ members = [ "arg_picker_macros", # Scaffolding tool - "mling" + # "mling" (deprecated) ] exclude = [ diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index e08d17b..1aad8d5 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -126,72 +126,26 @@ fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult { --- -## 4. Parsing Arguments — The Picker +## 4. Argument Parsing — Picker -Mingling provides a **Picker** for zero-cost argument extraction. You use `pick()` or `pick_or()` on an entry to extract typed values, then `unpack()` to get the final tuple. +Mingling provides a **Picker** for argument extraction. You can use `pick()` or `pick_or()` on an entry to tag typed values, then parse them into a tuple type all at once. ```rust -// Features: ["parser"] - -use mingling::parser::Picker; - +// Features: ["picker"] dispatcher!("greet", CMDGreet => EntryGreet); pack!(ResultGreeting = String); #[chain] fn handle_greet(args: EntryGreet) -> Next { - let (name, count) = Picker::new(args.inner) - .pick::<String>(()) // positional: first string - .pick_or::<u8>(["-r", "--repeat"], 1) // optional flag with default - .unpack(); - ResultGreeting::new(format!("{} x{}", name, count)).into() -} -``` - -With the `parser` feature, the `AsPicker` trait provides a shorthand directly on entries: - -```rust -// Features: ["parser"] - -dispatcher!("greet", CMDGreet => EntryGreet); -pack!(ResultGreeting = String); - -#[chain] -fn handle(args: EntryGreet) -> Next { let (name, count) = args - .pick::<Option<String>>(()) - .pick_or::<u8>(["-r", "--repeat"], 1) - .unpack(); - ResultGreeting::new(format!("{} x{}", name.unwrap_or_default(), count)).into() + .pick(&arg![String]) // positional argument: first string + .pick_or(&arg![repeat: u8, 'r'], || 1) // optional flag with default value + .unwrap(); + ResultGreeting::new(format!("{} x{}", name, count)).into() } ``` -For enums, derive `EnumTag` and implement `PickableEnum` to parse enum variants from strings: - -```rust -// Features: ["parser", "extra_macros"] - -use mingling::{EnumTag, Grouped}; -use mingling::parser::PickableEnum; - -dispatcher!("lang.select", CMDLang => EntryLang); - -#[derive(Debug, Default, EnumTag, Grouped)] -pub enum Language { - #[default] - Rust, - #[enum_rename("C++")] - CPlusPlus, -} - -impl PickableEnum for Language {} - -#[chain] -fn handle(args: EntryLang) -> Next { - let lang: Language = args.pick(()).unpack(); - lang.into() -} -``` +For more details on the Picker, see [arg-picker](https://github.com/mingling-rs/mingling/tree/main/arg_picker) --- @@ -369,7 +323,7 @@ Two built-in fallback types are always available: Chain and renderer functions can accept **additional parameters** for the program's global state. Resources are singleton values registered with `program.with_resource(...)`. ```rust -// Features: ["parser", "extra_macros"] +// Features: ["picker", "extra_macros"] use std::path::PathBuf; @@ -401,7 +355,7 @@ fn show_current(_prev: EntryCurrent, current_dir: &ResCurrentDir) -> Next { // Mutable access: #[chain] fn change_dir(prev: EntryCd, current_dir: &mut ResCurrentDir) -> Next { - let path: String = prev.pick(()).unpack(); + let path = prev.pick_or_default(&arg![String]).unwrap(); current_dir.current_dir = current_dir.current_dir.join(path); empty_result!() } @@ -618,11 +572,13 @@ fn main() { With the `structural_renderer` feature, users can add `--json` or `--yaml` flags to get structured output instead of human-readable text. ```rust -// Features: ["structural_renderer", "parser"] +// Features: ["structural_renderer", "picker"] // Dependencies: // serde = "1" -use mingling::{prelude::*, setup::StructuralRendererSetup}; +use mingling::prelude::*; +use mingling::macros::buffer; +use mingling::setup::picker::StructuralRendererSetup; use mingling::Grouped; use mingling::StructuralData; use serde::Serialize; @@ -638,15 +594,16 @@ struct ResultInfo { #[chain] fn render_info(args: EntryRender) -> Next { - let (name, age) = args.pick::<String>(()).pick::<i32>(()).unpack(); + let (name, age) = args + .pick_or_default(&arg![String]) + .pick_or_default(&arg![i32]) + .unwrap(); ResultInfo { name, age }.to_chain() } -#[renderer] -fn render_info_result(info: ResultInfo) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "{} is {} years old", info.name, info.age).ok(); - result +#[renderer(buffer)] +fn render_info_result(info: ResultInfo) { + r_println!("{} is {} years old", info.name, info.age); } fn main() { @@ -678,7 +635,7 @@ age: 22 Enable the `async` feature to use `async fn` inside `#[chain]`: ```rust -// Features: ["async", "parser"] +// Features: ["async", "picker"] // Dependencies: // tokio = { version = "1", features = ["full"] } @@ -690,7 +647,7 @@ pack!(ResultDownloaded = String); #[chain] pub async fn handle_download(args: EntryDownload) -> Next { - let file = args.pick(()).unpack(); + let file = args.pick_or_default(&arg![String]).unwrap(); download_file(file).await.into() } @@ -1,6 +1,6 @@ <p align="center"> <a href="https://github.com/mingling-rs/mingling"> - <img alt="Mingling" src="https://github.com/mingling-rs/mingling/raw/main/docs/res/icon2.png" width="30%"> + <img alt="Mingling" src="https://github.com/mingling-rs/mingling/raw/main/docs/res/icon3.png" width="30%"> </a> </p> <h1 align="center">Mìng Lìng - 命令</h1> @@ -62,6 +62,9 @@ You can use this approach to separate computation from result rendering, like th ```rust // Features: ["picker"] +use mingling::macros::buffer; +use mingling::prelude::*; + dispatcher!("calc", CMDCalculate => EntryCalculate); pack!(StateSumNumbers = Vec<i32>); pack!(ResultNumber = i32); @@ -82,11 +85,9 @@ fn handle_state_sum_numbers(sum: StateSumNumbers) -> ResultNumber { } // Renderer: return the render result and let the framework handle output -#[renderer] -fn render_number(number: ResultNumber) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "Number: {}", *number).ok(); - result +#[renderer(buffer)] +fn render_number(number: ResultNumber) { + r_println!("Number: {}", *number); } ``` @@ -117,16 +118,16 @@ features = [] ## Roadmap - [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 - - [x] [[0.1.7](https://docs.rs/mingling/0.1.7/mingling/)] [`clap`] Provides a **Clap** compatibility layer, allowing **Mingling** to reuse its powerful parsing capabilities - - [x] [[0.1.7](https://docs.rs/mingling/0.1.7/mingling/)] [`core`] **Mingling** can intercept `-h` or `--help` flags to display custom help text for each subcommand - - [x] [[0.1.7](https://docs.rs/mingling/0.1.7/mingling/)] [`mling`] Provides a basic scaffolding tool (`mling`) for rapid development and debugging - - [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();`) - - [x] [[0.2.0](https://docs.rs/mingling/0.2.0/mingling/)] Complete documentation, tests, and examples + - [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 + - [x] [[0.1.7](https://docs.rs/mingling/0.1.7/mingling/)] [`clap`] Provides a **Clap** compatibility layer, allowing **Mingling** to reuse its powerful parsing capabilities + - [x] [[0.1.7](https://docs.rs/mingling/0.1.7/mingling/)] [`core`] **Mingling** can intercept `-h` or `--help` flags to display custom help text for each subcommand + - [x] [[0.1.7](https://docs.rs/mingling/0.1.7/mingling/)] [`mling`] Provides a basic scaffolding tool (`mling`) for rapid development and debugging + - [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();`) + - [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" - [ ] [`mling` / `mingling-cli`] - [ ] **Mingling** Linter @@ -134,7 +135,8 @@ features = [] - [ ] **Mingling** Program Installer & Manager (For development) - [ ] Helpdoc Editor - [ ] [`picker`] A more efficient and intelligent argument parser - - [x] [`macros`] Remove `r_print!` / `r_println!` macros + - [x] [`macros`] ~~Remove r_print! / r_println! macros~~ (see below) + - [x] [`macros`] Make implicit modifications to functions explicit - [ ] Milestone.3 "Unplanned" - [ ] ... @@ -163,4 +165,5 @@ See [LICENSE-MIT](LICENSE-MIT) or [LICENSE-APACHE](LICENSE-APACHE) file for deta - 💡 Examples - [Github](https://mingling-rs.github.io/mingling/docs/examples.html) - 📖 Help Doc - [EN](https://mingling-rs.github.io/mingling/docs/doc.html#/) | [中文](https://mingling-rs.github.io/mingling/docs/_zh_CN/index.html#/) - 📖 API Doc - [docs.rs](https://docs.rs/mingling/latest/mingling/) | [latest](https://mingling-rs.github.io/mingling/docs/api-docs/mingling/) +- 📖 Coverage Test - [LLVM Coverage](https://mingling-rs.github.io/mingling/docs/cov-test/) - 📖 Dev Doc - [Github](https://mingling-rs.github.io/mingling/docs/dev/) diff --git a/arg_picker/src/arg.rs b/arg_picker/src/arg.rs index a352418..4fa21b5 100644 --- a/arg_picker/src/arg.rs +++ b/arg_picker/src/arg.rs @@ -1,4 +1,4 @@ -use crate::Pickable; +use crate::{Pickable, PickerArgInfo}; use std::marker::PhantomData; /// Represents a constraint definition for a parameter selection. @@ -133,6 +133,35 @@ where self.positional = positional; self } + + /// Converts this `PickerArg` into a `PickerArgInfo` value. + /// + /// This is a convenience method equivalent to calling `PickerArgInfo::from(self)`. + pub fn into_info(self) -> PickerArgInfo<'a> { + let value = 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) + } + }; + + PickerArgInfo { + short: value.short, + long, + alias, + positional: value.positional, + optional: false, + multi: false, + is_flag: false, + } + } } /// Describes the attribute (behavior) of a command-line parameter. diff --git a/arg_picker/src/constants.rs b/arg_picker/src/constants.rs new file mode 100644 index 0000000..39250f8 --- /dev/null +++ b/arg_picker/src/constants.rs @@ -0,0 +1,16 @@ +use std::marker::PhantomData; + +use crate::{PickerArg, PickerArgs}; + +/// Remaining positional arguments (anything not consumed as an option). +/// - `full`: `[]` (empty — not triggered by any `--` prefix). +/// - `short`: (none) +/// - `positional`: `false` (this is a meta‑argument that collects everything left). +/// This constant is used internally to access any leftover arguments after +/// all defined flags/options have been processed. +pub const REMAINS: PickerArg<PickerArgs> = PickerArg::<PickerArgs> { + full: &[], + short: None, + positional: false, + internal_type: PhantomData, +}; diff --git a/arg_picker/src/infos.rs b/arg_picker/src/infos.rs index 074539a..c17b6a2 100644 --- a/arg_picker/src/infos.rs +++ b/arg_picker/src/infos.rs @@ -1,4 +1,4 @@ -use crate::{Pickable, PickerArg}; +use crate::{Pickable, PickerArg, parselib::ParserStyle}; /// Represents the result of parsing or looking up a value. /// @@ -288,28 +288,7 @@ 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, - } + value.into_info() } } @@ -437,6 +416,56 @@ impl<'a> PickerArgInfo<'a> { self.is_flag = is_flag; self } + + /// Returns the short flag string, e.g. `-n` for short `n`. + /// + /// Uses [`ParserStyle::global_style()`] to format the flag. + /// + /// # Returns + /// + /// - `Some(String)` if `self.short` is set. + /// - `None` if `self.short` is `None`. + /// + /// # Examples + /// + /// ``` + /// use arg_picker::PickerArgInfo; + /// + /// let info = PickerArgInfo::new().with_short('n'); + /// assert_eq!(info.short_flag(), Some("-n".to_string())); + /// + /// let info = PickerArgInfo::new(); + /// assert_eq!(info.short_flag(), None); + /// ``` + pub fn short_flag(&self) -> Option<String> { + let short = self.short?; + Some(ParserStyle::global_style().flag_string(short)) + } + + /// Returns the long flag string, e.g. `--name` for long `"name"`. + /// + /// Uses [`ParserStyle::global_style()`] to format the flag. + /// + /// # Returns + /// + /// - `Some(String)` if `self.long` is set. + /// - `None` if `self.long` is `None`. + /// + /// # Examples + /// + /// ``` + /// use arg_picker::PickerArgInfo; + /// + /// let info = PickerArgInfo::new().with_long("name"); + /// assert_eq!(info.long_flag(), Some("--name".to_string())); + /// + /// let info = PickerArgInfo::new(); + /// assert_eq!(info.long_flag(), None); + /// ``` + pub fn long_flag(&self) -> Option<String> { + let long = self.long?; + Some(ParserStyle::global_style().flag_string(long)) + } } impl<'a> Default for PickerArgInfo<'a> { diff --git a/arg_picker/src/lib.rs b/arg_picker/src/lib.rs index c65e793..d292826 100644 --- a/arg_picker/src/lib.rs +++ b/arg_picker/src/lib.rs @@ -1,4 +1,5 @@ #![doc = include_str!("../README.md")] +#![deny(missing_docs)] mod builtin; @@ -48,3 +49,13 @@ pub mod matcher_needed { pub use crate::PickerArgInfo; pub use crate::parselib::{MaskedArg, Matcher, ParserStyle}; } + +mod constants; + +/// Re-export of constants used by `arg-picker`. +/// +/// This module provides access to various constants defined internally, such as +/// default values, configuration limits, and other static parameters. +pub mod consts { + pub use crate::constants::*; +} diff --git a/arg_picker/src/pickable/multi_pickable.rs b/arg_picker/src/pickable/multi_pickable.rs index 84a8068..404c23b 100644 --- a/arg_picker/src/pickable/multi_pickable.rs +++ b/arg_picker/src/pickable/multi_pickable.rs @@ -5,13 +5,29 @@ use crate::{ }; /// Boundary check for multi-value positional parameters. +/// +/// Determines whether a raw string marks the end of a multi-value +/// parameter's input range. pub trait BoundaryCheck { + /// Returns `true` if `raw` indicates a boundary (i.e., the start of + /// a new parameter), stopping greedy collection. fn check_boundary(raw: &str) -> bool; } /// Trait for multi-value parameters. +/// +/// Implementors define how a sequence of raw strings is converted into +/// a single value, with an associated [`BoundaryCheck`] to control where +/// collection stops. pub trait MultiPickableWithBoundary: Sized { + /// The boundary checker type that determines when to stop consuming + /// positional arguments. type Checker: BoundaryCheck; + + /// Parse and collect multiple raw string values into `Self`. + /// + /// The caller should stop passing additional items once the + /// associated [`Checker`](Self::Checker) signals a boundary. fn pick_multi(raw: Vec<String>) -> PickerArgResult<Self>; } diff --git a/arg_picker/src/picker.rs b/arg_picker/src/picker.rs index f31a5b6..f722fb5 100644 --- a/arg_picker/src/picker.rs +++ b/arg_picker/src/picker.rs @@ -369,11 +369,83 @@ pub trait IntoPicker<'a> { fn pick<N>(self, arg: impl Into<&'a PickerArg<'a, N>>) -> PickerPattern1<'a, N, ()> where Self: Sized, - N: Pickable<'a> + Default + Sized, + N: Pickable<'a> + Sized, { Picker::build_pattern1(self.to_picker().args, arg.into(), None::<()>) } + /// Starts building a picker pattern with the first argument, using a default value provider. + /// + /// This is a shorthand for calling `.pick(arg).or(func)`. + /// + /// # Type Parameters + /// + /// * `Next` — The nominal type of the first argument; must implement [`Pickable`]. + /// + /// # Parameters + /// + /// * `arg` — The argument definition, typically obtained from [`crate::macros::arg`]. + /// * `func` — A closure that provides a default value if the arg is not provided by the user. + fn pick_or<Next, F>( + self, + arg: impl Into<&'a PickerArg<'a, Next>>, + func: F, + ) -> PickerPattern1<'a, Next, ()> + where + Self: Sized, + Next: Pickable<'a> + Sized, + F: FnMut() -> Next + 'static, + { + self.pick(arg).or(func) + } + + /// Starts building a picker pattern with the first argument, using a default value. + /// + /// This is a shorthand for calling `.pick(arg).or_default()`. + /// + /// # Type Parameters + /// + /// * `Next` — The nominal type of the first argument; must implement [`Pickable`] and [`Default`]. + /// + /// # Parameters + /// + /// * `arg` — The argument definition, typically obtained from [`crate::macros::arg`]. + fn pick_or_default<Next>( + self, + arg: impl Into<&'a PickerArg<'a, Next>>, + ) -> PickerPattern1<'a, Next, ()> + where + Self: Sized, + Next: Pickable<'a> + Default + Sized, + { + self.pick(arg).or_default() + } + + /// Starts building a picker pattern with the first argument, using a route if the arg is not provided. + /// + /// This is a shorthand for calling `.pick(arg).or_route(func)`. + /// + /// # Type Parameters + /// + /// * `Next` — The nominal type of the first argument; must implement [`Pickable`]. + /// + /// # Parameters + /// + /// * `arg` — The argument definition, typically obtained from [`crate::macros::arg`]. + /// * `func` — A closure that produces a route value if the arg is not provided by the user. + fn pick_or_route<Next, F, Route>( + self, + arg: impl Into<&'a PickerArg<'a, Next>>, + func: F, + ) -> PickerPattern1<'a, Next, Route> + where + Self: Sized, + Next: Pickable<'a> + Sized, + F: FnMut() -> Route + 'static, + { + self.pick(arg).with_route::<Route>().or_route(func) + } + /// Converts the value into a `Picker` with a specified route type. /// /// This method allows changing the route (phantom type parameter) of the picker. diff --git a/arg_picker/src/value/vec_until.rs b/arg_picker/src/value/vec_until.rs index 1b79641..04d87ce 100644 --- a/arg_picker/src/value/vec_until.rs +++ b/arg_picker/src/value/vec_until.rs @@ -21,6 +21,7 @@ pub struct VecUntil<T> { } impl<T> VecUntil<T> { + /// Consumes `self` and returns the underlying [`Vec<T>`]. pub fn into_inner(self) -> Vec<T> { self.inner } diff --git a/arg_picker_macros/src/arg.rs b/arg_picker_macros/src/arg.rs index 55860c4..20731b1 100644 --- a/arg_picker_macros/src/arg.rs +++ b/arg_picker_macros/src/arg.rs @@ -119,6 +119,7 @@ pub(crate) fn arg(input: TokenStream) -> TokenStream { fn split_at_commas(tokens: &[TokenTree]) -> Vec<Vec<TokenTree>> { let mut result = vec![Vec::new()]; let mut depth = 0u32; + let mut angle_depth = 0u32; for t in tokens { match t { TokenTree::Group(_g) => { @@ -126,7 +127,15 @@ fn split_at_commas(tokens: &[TokenTree]) -> Vec<Vec<TokenTree>> { result.last_mut().unwrap().push(t.clone()); depth -= 1; } - TokenTree::Punct(p) if p.as_char() == ',' && depth == 0 => { + TokenTree::Punct(p) if p.as_char() == '<' => { + angle_depth += 1; + result.last_mut().unwrap().push(t.clone()); + } + TokenTree::Punct(p) if p.as_char() == '>' && angle_depth > 0 => { + angle_depth -= 1; + result.last_mut().unwrap().push(t.clone()); + } + TokenTree::Punct(p) if p.as_char() == ',' && depth == 0 && angle_depth == 0 => { result.push(Vec::new()); } _ => result.last_mut().unwrap().push(t.clone()), diff --git a/arg_picker_macros/src/lib.rs b/arg_picker_macros/src/lib.rs index 18cce84..0132e66 100644 --- a/arg_picker_macros/src/lib.rs +++ b/arg_picker_macros/src/lib.rs @@ -1,4 +1,5 @@ #![doc = include_str!("../README.md")] +#![deny(missing_docs)] use proc_macro::TokenStream; diff --git a/docs/_zh_CN/pages/10-help.md b/docs/_zh_CN/pages/10-help.md index 8cee2aa..1c4e410 100644 --- a/docs/_zh_CN/pages/10-help.md +++ b/docs/_zh_CN/pages/10-help.md @@ -13,18 +13,17 @@ Mingling 里用 `#[help]` 宏给命令添加帮助文本。 ```rust @@@use mingling::macros::help; +@@@use mingling::macros::buffer; @@@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 +#[help(buffer)] +fn help_greet(_entry: EntryGreet) { + r_println!("Usage: greet [name]"); + r_println!("Say hello to someone."); } ``` > [!NOTE] -> 帮助函数同样通过 `writeln!` 向 `RenderResult` 写入内容,因为 `#[help]` 遵循渲染管线 —— 它是由 `--help` 标志提前触发的短路渲染,而不是管线之外的逻辑。 +> 帮助函数同样通过 `r_println!` 向 `RenderResult` 写入内容,因为 `#[help]` 遵循渲染管线 —— 它是由 `--help` 标志提前触发的短路渲染,而不是管线之外的逻辑。 ## 全局帮助 @@ -32,14 +31,13 @@ fn help_greet(_entry: EntryGreet) -> RenderResult { ```rust @@@use mingling::macros::help; +@@@use mingling::macros::buffer; // 用户直接输入 --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 +#[help(buffer)] +fn help_root(entry: ErrorDispatcherNotFound) { + r_println!("Usage: my-cli <command>"); + r_println!("Commands:"); + r_println!(" greet Say hello"); } ``` diff --git a/docs/_zh_CN/pages/11-resource-system.md b/docs/_zh_CN/pages/11-resource-system.md index a8aab4d..b127a3a 100644 --- a/docs/_zh_CN/pages/11-resource-system.md +++ b/docs/_zh_CN/pages/11-resource-system.md @@ -27,6 +27,7 @@ fn main() { 在 Chain 或 Renderer 中,只需在参数列表里声明你要的资源: ```rust +@@@use mingling::macros::buffer; @@@#[derive(Default, Clone)] @@@struct ResCurrentDir(String); @@@dispatcher!("pwd", CMDPrintWorkingDir => EntryPrintWorkingDir); @@ -37,11 +38,9 @@ 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 +#[renderer(buffer)] +fn render_path(result: ResultPath) { + r_println!("{}", *result); } ``` @@ -50,6 +49,7 @@ fn render_path(result: ResultPath) -> RenderResult { 用 `&mut T` 注入可修改资源: ```rust +@@@use mingling::macros::buffer; @@@#[derive(Default, Clone)] @@@struct ResVisitCount(u32); @@@dispatcher!("visit", CMDVisit => EntryVisit); @@ -60,11 +60,9 @@ fn handle_visit(_args: EntryVisit, counter: &mut ResVisitCount) -> Next { ResultDone::default().into() } -#[renderer] -fn render_done(_done: ResultDone, counter: &ResVisitCount) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "visit count is : {}", counter.0).ok(); - r +#[renderer(buffer)] +fn render_done(_done: ResultDone, counter: &ResVisitCount) { + r_println!("visit count is : {}", counter.0); } ``` diff --git a/docs/_zh_CN/pages/14-testing.md b/docs/_zh_CN/pages/14-testing.md index 621d43e..031be59 100644 --- a/docs/_zh_CN/pages/14-testing.md +++ b/docs/_zh_CN/pages/14-testing.md @@ -16,7 +16,7 @@ Renderer 是最容易测试的——调用函数,断言返回结果: #[renderer] fn render_greet(result: ResultName) -> RenderResult { let mut r = RenderResult::new(); - writeln!(r, "Hello, {}!", *result).ok(); + r_println!(r, "Hello, {}!", *result); r } diff --git a/docs/_zh_CN/pages/4-render-result.md b/docs/_zh_CN/pages/4-render-result.md index 81c5102..8b29fca 100644 --- a/docs/_zh_CN/pages/4-render-result.md +++ b/docs/_zh_CN/pages/4-render-result.md @@ -10,18 +10,31 @@ 跟 `#[chain]` 类似,`#[renderer]` 用于标记一个输出函数: ```rust -use std::io::Write; +@@@use mingling::macros::buffer; +@@@pack!(ResultName = String); +#[renderer(buffer)] +fn render_name(name: ResultName) { + r_println!("Hello, {}!", *name); +} +``` + +Renderer 接收 Chain 产出的结果,然后返回一个 `RenderResult`。在函数内部,创建 `RenderResult`,用 `r_print!` / `r_println!` 写入内容,最后返回它。 + +## `buffer` 拓展 + +若您觉得显式创建并返回 `RenderResult` 过于繁琐,可以使用 `#[renderer(buffer)]` 在原始函数内植入一个缓冲区。 + +```rust +use mingling::macros::buffer; @@@pack!(ResultName = String); -#[renderer] -fn render_name(name: ResultName) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "Hello, {}!", *name).ok(); - result +#[renderer(buffer)] +fn render_name(name: ResultName) { + r_println!("Hello, {}!", *name); } ``` -Renderer 接收 Chain 产出的结果,然后返回一个 `RenderResult`。在函数内部,创建 `RenderResult`,用 `write!` / `writeln!`(来自 [`std::io::Write`](https://doc.rust-lang.org/std/io/trait.Write.html))写入内容,最后返回它。 +这样,你的 renderer 函数就获得了更简洁的语法,但也引入了一个隐含机制:它会向函数内注入一个名为 `__render_result_buffer` 的可变 `RenderResult`。当 `r_print!` 宏没有显式指定 `RenderResult` 时,它便会按照约定向该缓冲区追加输出。 ## `RenderResult` 类型 @@ -36,7 +49,7 @@ Renderer 接收 Chain 产出的结果,然后返回一个 `RenderResult`。在 把三篇教程的内容合在一起,你的第一个 Mingling 程序就完整了: ```rust -use std::io::Write; +use mingling::macros::buffer; // 1. 用 Dispatcher 声明命令 dispatcher!("greet", CMDGreet => EntryGreet); @@ -55,11 +68,9 @@ fn handle_greet(args: EntryGreet) -> Next { } // 4. 用 Renderer 输出结果 -#[renderer] -fn render_name(name: ResultName) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "Hello, {}!", *name).ok(); - result +#[renderer(buffer)] +fn render_name(name: ResultName) { + r_println!("Hello, {}!", *name); } // 5. 在 main 函数内装配程序并运行 @@ -108,17 +119,15 @@ cargo run -- great `gen_program!()` 自动生成了一个 `ErrorDispatcherNotFound` 类型,包裹 `Vec<String>`——它存的是用户输入的那些没匹配到的命令。你只需要给它写一个 Renderer: ```rust -use std::io::Write; +use mingling::macros::buffer; -#[renderer] -fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult { - let mut result = RenderResult::new(); +#[renderer(buffer)] +fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) { if err.inner.is_empty() { - writeln!(result, "Unknown command").ok(); + r_println!("Unknown command"); } else { - writeln!(result, "Command not found: \"{}\"", err.inner.join(" ")).ok(); + r_println!("Command not found: \"{}\"", err.inner.join(" ")); } - result } ``` diff --git a/docs/_zh_CN/pages/5-multiple-commands.md b/docs/_zh_CN/pages/5-multiple-commands.md index 38ce2cf..ff6596d 100644 --- a/docs/_zh_CN/pages/5-multiple-commands.md +++ b/docs/_zh_CN/pages/5-multiple-commands.md @@ -10,6 +10,7 @@ 继续在同一个项目里操作: ```rust +@@@use mingling::macros::buffer; // 声明两个命令 dispatcher!("greet", CMDGreet => EntryGreet); dispatcher!("add", CMDAdd => EntryAdd); @@ -29,18 +30,14 @@ fn handle_add(args: EntryAdd) -> Next { ResultSum::new(sum).into() } -#[renderer] -fn render_greet(result: ResultGreeting) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "Hello, {}!", *result).ok(); - r +#[renderer(buffer)] +fn render_greet(result: ResultGreeting) { + r_println!("Hello, {}!", *result); } -#[renderer] -fn render_sum(result: ResultSum) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "Sum: {}", *result).ok(); - r +#[renderer(buffer)] +fn render_sum(result: ResultSum) { + r_println!("Sum: {}", *result); } fn main() { diff --git a/docs/_zh_CN/pages/6-argument-parse-picker.md b/docs/_zh_CN/pages/6-argument-parse-picker.md index 4bf162c..f0609ed 100644 --- a/docs/_zh_CN/pages/6-argument-parse-picker.md +++ b/docs/_zh_CN/pages/6-argument-parse-picker.md @@ -139,6 +139,7 @@ fn handle_test_entry(prev: EntryTest) -> Next { ```rust // Features: ["parser", "extra_macros"] +@@@use mingling::macros::buffer; @@@use mingling::macros::route; @@@dispatcher!("greet", CMDGreet => EntryGreet); @@@pack!(ResultName = String); @@ -155,11 +156,9 @@ fn handle_greet_entry(prev: EntryGreet) -> Next { ResultName::new(name).into() } -#[renderer] -fn render_greet(result: ResultName) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "Hello, {}!", *result).ok(); - r +#[renderer(buffer)] +fn render_greet(result: ResultName) { + r_println!("Hello, {}!", *result); } ``` @@ -225,6 +224,7 @@ fn handle_greet_entry(prev: EntryGreet) -> Next { ```rust // Features: ["parser", "extra_macros"] +@@@use mingling::macros::buffer; @@@use mingling::macros::route; @@@dispatcher!("greet", CMDGreet => EntryGreet); @@@pack!(ResultName = String); @@ -247,19 +247,15 @@ fn handle_greet_entry(prev: EntryGreet) -> Next { ResultName::new(name).into() } -#[renderer] -fn render_name_too_long(prev: ErrorNameTooLong) -> RenderResult { - let mut r = RenderResult::new(); +#[renderer(buffer)] +fn render_name_too_long(prev: ErrorNameTooLong) { let len = *prev; - writeln!(r, "Error: name too long (length: {} > 32)", len).ok(); - r + r_println!("Error: name too long (length: {} > 32)", len); } -#[renderer] -fn render_name(prev: ResultName) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "Hello, {}!", *prev).ok(); - r +#[renderer(buffer)] +fn render_name(prev: ResultName) { + r_println!("Hello, {}!", *prev); } ``` @@ -314,6 +310,7 @@ fn parse_size() { ```rust // Features: ["parser"] +@@@use mingling::macros::buffer; @@@use mingling::parser::{Pickable, Argument}; @@@use mingling::Flag; #[derive(Default, Clone)] @@ -341,11 +338,9 @@ fn handle_connect_entry(prev: EntryConnect) -> Next { ResultConnected::new(address).into() } -#[renderer] -fn render_connected(addr: ResultConnected) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "Connected: IP: {} PORT: {}", addr.ip, addr.port).ok(); - r +#[renderer(buffer)] +fn render_connected(addr: ResultConnected) { + r_println!("Connected: IP: {} PORT: {}", addr.ip, addr.port); } ``` @@ -362,6 +357,7 @@ Connected: IP: 127.0.0.1 PORT: 8080 ```rust // Features: ["parser"] +@@@use mingling::macros::buffer; @@@use mingling::parser::PickableEnum; @@@use mingling::EnumTag; #[derive(Debug, Default, EnumTag)] @@ -382,11 +378,9 @@ fn handle_eat_entry(prev: EntryEat) -> Next { ResultFruit::new(fruit).into() } -#[renderer] -fn render_fruit(prev: ResultFruit) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "Picked fruit: {:?}", *prev).ok(); - r +#[renderer(buffer)] +fn render_fruit(prev: ResultFruit) { + r_println!("Picked fruit: {:?}", *prev); } ``` @@ -395,3 +389,4 @@ fn render_fruit(prev: ResultFruit) -> RenderResult { <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 index 21f886c..fda93f3 100644 --- a/docs/_zh_CN/pages/7-argument-parse-clap.md +++ b/docs/_zh_CN/pages/7-argument-parse-clap.md @@ -25,6 +25,7 @@ features = ["derive", "color"] // Dependencies: // clap = "4" @@@ use mingling::macros::dispatcher_clap; +@@@ use mingling::macros::buffer; #[derive(Default, clap::Parser, Grouped)] #[dispatcher_clap("greet", CMDGreet, help = true, error = ErrorGreetParsed)] pub struct EntryGreet { @@ -34,23 +35,19 @@ pub struct EntryGreet { repeat: i32, } -#[renderer] -fn render_greet(greet: EntryGreet) -> RenderResult { - let mut r = RenderResult::new(); +#[renderer(buffer)] +fn render_greet(greet: EntryGreet) { let count = greet.repeat.max(0) as usize; - write!(r, "Hello, ").ok(); + r_print!("Hello, "); for _ in 0..count { - write!(r, "{} ", greet.name).ok(); + r_print!("{} ", greet.name); } - writeln!(r, "!").ok(); - r + r_println!("!"); } -#[renderer] -fn render_greet_parse_failed(err: ErrorGreetParsed) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "{}", *err).ok(); - r +#[renderer(buffer)] +fn render_greet_parse_failed(err: ErrorGreetParsed) { + r_println!("{}", *err); } ``` @@ -62,6 +59,7 @@ fn render_greet_parse_failed(err: ErrorGreetParsed) -> RenderResult { // Features: ["clap"] // Dependencies: // clap = "4" +@@@use mingling::macros::buffer; @@@use mingling::setup::BasicProgramSetup; @@@use mingling::macros::dispatcher_clap; @@@#[derive(Default, clap::Parser, Grouped)] @@ -69,11 +67,9 @@ fn render_greet_parse_failed(err: ErrorGreetParsed) -> RenderResult { @@@pub struct EntryGreet { @@@ name: String, @@@} -@@@#[renderer] -@@@fn render_greet(greet: EntryGreet) -> RenderResult { -@@@ let mut r = RenderResult::new(); -@@@ write!(r, "Hello, {}!", greet.name).ok(); -@@@ r +@@@#[renderer(buffer)] +@@@fn render_greet(greet: EntryGreet) { +@@@ r_println!("Hello, {}!", greet.name); @@@} fn main() { let mut program = ThisProgram::new(); diff --git a/docs/_zh_CN/pages/9-error-handling.md b/docs/_zh_CN/pages/9-error-handling.md index 07c82da..4ce61ab 100644 --- a/docs/_zh_CN/pages/9-error-handling.md +++ b/docs/_zh_CN/pages/9-error-handling.md @@ -38,23 +38,20 @@ fn handle_greet(args: EntryGreet) -> Next { 然后各自写 Renderer: ```rust +@@@use mingling::macros::buffer; @@@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(buffer)] +fn render_greet(result: ResultGreeting) { + r_println!("Hello, {}!", *result); } -#[renderer] -fn render_error_name_empty(err: ErrorNameEmpty) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "Error: {}", *err).ok(); - r +#[renderer(buffer)] +fn render_error_name_empty(err: ErrorNameEmpty) { + r_println!("Error: {}", *err); } ``` @@ -63,6 +60,7 @@ fn render_error_name_empty(err: ErrorNameEmpty) -> RenderResult { ## 完整的例子 ```rust +@@@use mingling::macros::buffer; dispatcher!("greet", CMDGreet => EntryGreet); pack!(ResultGreeting = String); @@ -78,18 +76,14 @@ fn handle_greet(args: EntryGreet) -> Next { } } -#[renderer] -fn render_greet(result: ResultGreeting) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "Hello, {}!", *result).ok(); - r +#[renderer(buffer)] +fn render_greet(result: ResultGreeting) { + r_println!("Hello, {}!", *result); } -#[renderer] -fn render_error_name_empty(err: ErrorNameEmpty) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "Error: {}", *err).ok(); - r +#[renderer(buffer)] +fn render_error_name_empty(err: ErrorNameEmpty) { + r_println!("Error: {}", *err); } fn main() { diff --git a/docs/_zh_CN/pages/advanced/2-structural-renderer.md b/docs/_zh_CN/pages/advanced/2-structural-renderer.md index 70bed79..a498dac 100644 --- a/docs/_zh_CN/pages/advanced/2-structural-renderer.md +++ b/docs/_zh_CN/pages/advanced/2-structural-renderer.md @@ -27,6 +27,7 @@ features = ["structural_renderer"] // Features: ["structural_renderer"] // Dependencies: // serde = "1" +@@@use mingling::macros::buffer; @@@use mingling::setup::StructuralRendererSetup; @@@dispatcher!("render", CMDRender => EntryRender); @@ -40,11 +41,9 @@ fn handle_render(args: EntryRender) -> Next { ResultInfo::new((name, age)).into() } -#[renderer] -fn render_info(r: ResultInfo) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "{:?}", *r).ok(); - result +#[renderer(buffer)] +fn render_info(r: ResultInfo) { + r_println!("{:?}", *r); } ``` @@ -68,6 +67,7 @@ fn render_info(r: ResultInfo) -> RenderResult { // Features: ["structural_renderer"] // Dependencies: // serde = "1" +@@@use mingling::macros::buffer; @@@use mingling::prelude::*; @@@use mingling::setup::StructuralRendererSetup; @@@use mingling::StructuralData; @@ -87,11 +87,9 @@ fn handle_render(args: EntryRender) -> Next { 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 +#[renderer(buffer)] +fn render_info(info: Info) { + r_println!("{} is {} years old", info.name, info.age); } @@@ @@@fn main() { diff --git a/docs/_zh_CN/pages/other/naming_rule.md b/docs/_zh_CN/pages/other/naming_rule.md index c854ba1..6e65ee4 100644 --- a/docs/_zh_CN/pages/other/naming_rule.md +++ b/docs/_zh_CN/pages/other/naming_rule.md @@ -166,6 +166,7 @@ fn handle_remote_add(args: EntryRemoteAdd, cwd: &ResCurrentDir, db: &mut ResData ## 完整示例 ```rust +@@@use mingling::macros::buffer; @@@ #[derive(Default, Clone)] @@@ struct ResDatabase { } @@@ impl ResDatabase { fn has_remote(&self, remote: &String) -> bool { true } } @@ -192,19 +193,15 @@ fn handle_state_operation_remotes(state: StateOperationRemotes, db: &ResDatabase } // 结果渲染 -#[renderer] -fn render_remote_added(result: ResultRemoteAdded) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "Remote added: {}", result.inner).ok(); - r +#[renderer(buffer)] +fn render_remote_added(result: ResultRemoteAdded) { + r_println!("Remote added: {}", result.inner); } // 错误渲染 -#[renderer] -fn render_error_repository_not_found(err: ErrorRepositoryNotFound) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "Error: remote '{}' not found", err.inner).ok(); - r +#[renderer(buffer)] +fn render_error_repository_not_found(err: ErrorRepositoryNotFound) { + r_println!("Error: remote '{}' not found", err.inner); } ``` diff --git a/docs/dev/pages/issues/add-picker2.md b/docs/dev/pages/issues/add-picker2.md index 6fe4418..a2b5a10 100644 --- a/docs/dev/pages/issues/add-picker2.md +++ b/docs/dev/pages/issues/add-picker2.md @@ -90,11 +90,11 @@ pub struct PickerResult<Tuple> { ## 🕘 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 + - [x] Added `Picker` struct and related call chain + - [x] Added `parselib` providing parsing logic + - [x] Added `Pickable` for extensibility + - [x] Comprehensive testing! + - [ ] Improve documentation + - [x] 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 index d6f6bff..43d2740 100644 --- a/docs/dev/pages/issues/remove-r-print-macro.md +++ b/docs/dev/pages/issues/remove-r-print-macro.md @@ -89,9 +89,9 @@ As for rendering in logic functions like `#[chain]`, that should be handled by a ## 🕘 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 + - [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 + - [x] Add new simplified syntax + - [x] Update documentation and test cases, ensure **all pass** +- [x] Complete diff --git a/docs/example-pages/examples.json b/docs/example-pages/examples.json index a539011..2a07365 100644 --- a/docs/example-pages/examples.json +++ b/docs/example-pages/examples.json @@ -31,6 +31,21 @@ ] }, { + "id": "example-argument-picker", + "name": "Argument Picker", + "icon": "📋", + "category": "parsing", + "desc": "Demonstrates how to use Mingling's `picker` feature and `Picker` to extract typed arguments from the command line.\n", + "tags": [ + "arg-picker", + "SinglePickable" + ], + "files": [ + "src/main.rs", + "Cargo.toml" + ] + }, + { "id": "example-async-support", "name": "Async Support", "icon": "⚡", diff --git a/docs/pages/10-help.md b/docs/pages/10-help.md index c17f410..1e3ea78 100644 --- a/docs/pages/10-help.md +++ b/docs/pages/10-help.md @@ -13,18 +13,17 @@ Write a help function directly for an Entry: ```rust @@@use mingling::macros::help; +@@@use mingling::macros::buffer; @@@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 +#[help(buffer)] +fn help_greet(_entry: EntryGreet) { + r_println!("Usage: greet [name]"); + r_println!("Say hello to someone."); } ``` > [!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. +> Help functions also use `r_println!` 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 @@ -32,14 +31,13 @@ You can also write help for `ErrorDispatcherNotFound` as the "root help": ```rust @@@use mingling::macros::help; +@@@use mingling::macros::buffer; // 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 +#[help(buffer)] +fn help_root(entry: ErrorDispatcherNotFound) { + r_println!("Usage: my-cli <command>"); + r_println!("Commands:"); + r_println!(" greet Say hello"); } ``` diff --git a/docs/pages/11-resource-system.md b/docs/pages/11-resource-system.md index 9491212..0e2bd19 100644 --- a/docs/pages/11-resource-system.md +++ b/docs/pages/11-resource-system.md @@ -27,6 +27,7 @@ Since `ResCurrentDir` implements both `Default` and `Clone`, the framework autom In a Chain or Renderer, simply declare the resource in the parameter list: ```rust +@@@use mingling::macros::buffer; @@@#[derive(Default, Clone)] @@@struct ResCurrentDir(String); @@@dispatcher!("pwd", CMDPrintWorkingDir => EntryPrintWorkingDir); @@ -37,11 +38,9 @@ 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 +#[renderer(buffer)] +fn render_path(result: ResultPath) { + r_println!("{}", *result); } ``` @@ -50,6 +49,7 @@ fn render_path(result: ResultPath) -> RenderResult { Use `&mut T` to inject a mutable resource: ```rust +@@@use mingling::macros::buffer; @@@#[derive(Default, Clone)] @@@struct ResVisitCount(u32); @@@dispatcher!("visit", CMDVisit => EntryVisit); @@ -60,11 +60,9 @@ fn handle_visit(_args: EntryVisit, counter: &mut ResVisitCount) -> Next { ResultDone::default().into() } -#[renderer] -fn render_done(_done: ResultDone, counter: &ResVisitCount) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "visit count is : {}", counter.0).ok(); - r +#[renderer(buffer)] +fn render_done(_done: ResultDone, counter: &ResVisitCount) { + r_println!("visit count is : {}", counter.0); } ``` diff --git a/docs/pages/14-testing.md b/docs/pages/14-testing.md index fa196b0..65fedc9 100644 --- a/docs/pages/14-testing.md +++ b/docs/pages/14-testing.md @@ -16,7 +16,7 @@ Renderer is the easiest to test — call the function, assert the result: #[renderer] fn render_greet(result: ResultName) -> RenderResult { let mut r = RenderResult::new(); - writeln!(r, "Hello, {}!", *result).ok(); + r_println!(r, "Hello, {}!", *result); r } diff --git a/docs/pages/4-render-result.md b/docs/pages/4-render-result.md index ca1e563..9e72d09 100644 --- a/docs/pages/4-render-result.md +++ b/docs/pages/4-render-result.md @@ -7,21 +7,34 @@ Now we've created a Dispatcher and a Chain, and produced a Result type via `pack ## The `#[renderer]` Macro -Similar to `#[chain]`, `#[renderer]` marks an output function: +Similar to `#[chain]`, `#[renderer]` marks a function that produces output: ```rust -use std::io::Write; +@@@use mingling::macros::buffer; +@@@pack!(ResultName = String); +#[renderer(buffer)] +fn render_name(name: ResultName) { + r_println!("Hello, {}!", *name); +} +``` -pack!(ResultName = String); -#[renderer] -fn render_name(name: ResultName) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "Hello, {}!", *name).ok(); - result +A Renderer receives the result produced by a Chain and returns a `RenderResult`. Inside the function, you create a `RenderResult`, write content with `r_print!` / `r_println!`, and finally return it. + +## The `buffer` Extension + +If you find explicitly creating and returning a `RenderResult` too verbose, you can use `#[renderer(buffer)]` to inject a buffer directly into your function. + +```rust +use mingling::macros::buffer; + +@@@pack!(ResultName = String); +#[renderer(buffer)] +fn render_name(name: ResultName) { + r_println!("Hello, {}!", *name); } ``` -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. +This gives your renderer function a more concise syntax, but also introduces an implicit mechanism: it injects a mutable `RenderResult` variable named `__render_result_buffer` into the function. When the `r_print!` macro is used without an explicit `RenderResult`, it will append output to that buffer by convention. ## The `RenderResult` Type @@ -36,7 +49,7 @@ A Renderer takes the result produced by a Chain and returns a `RenderResult`. In Putting all three tutorials together, here's your first complete Mingling program: ```rust -use std::io::Write; +use mingling::macros::buffer; // 1. Declare commands with a Dispatcher dispatcher!("greet", CMDGreet => EntryGreet); @@ -55,11 +68,9 @@ fn handle_greet(args: EntryGreet) -> Next { } // 4. Output results with a Renderer -#[renderer] -fn render_name(name: ResultName) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "Hello, {}!", *name).ok(); - result +#[renderer(buffer)] +fn render_name(name: ResultName) { + r_println!("Hello, {}!", *name); } // 5. Assemble and run the program in main @@ -108,17 +119,15 @@ cargo run -- great `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; +use mingling::macros::buffer; -#[renderer] -fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult { - let mut result = RenderResult::new(); +#[renderer(buffer)] +fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) { if err.inner.is_empty() { - writeln!(result, "Unknown command").ok(); + r_println!("Unknown command"); } else { - writeln!(result, "Command not found: \"{}\"", err.inner.join(" ")).ok(); + r_println!("Command not found: \"{}\"", err.inner.join(" ")); } - result } ``` diff --git a/docs/pages/5-multiple-commands.md b/docs/pages/5-multiple-commands.md index d9a335a..0b71e49 100644 --- a/docs/pages/5-multiple-commands.md +++ b/docs/pages/5-multiple-commands.md @@ -10,6 +10,7 @@ Real-world CLIs rarely have just one command. Let's extend our previous greet pr Work in the same project: ```rust +@@@use mingling::macros::buffer; // Declare two commands dispatcher!("greet", CMDGreet => EntryGreet); dispatcher!("add", CMDAdd => EntryAdd); @@ -29,18 +30,14 @@ fn handle_add(args: EntryAdd) -> Next { ResultSum::new(sum).into() } -#[renderer] -fn render_greet(result: ResultGreeting) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "Hello, {}!", *result).ok(); - r +#[renderer(buffer)] +fn render_greet(result: ResultGreeting) { + r_println!("Hello, {}!", *result); } -#[renderer] -fn render_sum(result: ResultSum) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "Sum: {}", *result).ok(); - r +#[renderer(buffer)] +fn render_sum(result: ResultSum) { + r_println!("Sum: {}", *result); } fn main() { diff --git a/docs/pages/6-argument-parse-picker.md b/docs/pages/6-argument-parse-picker.md index 398cd3c..01f1c37 100644 --- a/docs/pages/6-argument-parse-picker.md +++ b/docs/pages/6-argument-parse-picker.md @@ -3,7 +3,7 @@ Use Picker to perform basic argument parsing </p> -In previous tutorials, we extracted args manually from `EntryGreet.inner` (`Vec<String>`). +In previous tutorials, we manually extracted parameters from `EntryGreet.inner` (`Vec<String>`). ```rust @@@ fn main() { @@ -12,7 +12,7 @@ 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. +But this approach doesn't scale well when there are many params. Mingling provides `Picker` — a chained API for extracting and transforming params. To enable `Picker`, update your `Cargo.toml`: @@ -22,7 +22,7 @@ To enable `Picker`, update your `Cargo.toml`: features = ["parser"] ``` -Now let's look at `Picker` in action: +Now let's see how `Picker` is written: ```rust // Features: ["parser"] @@ -36,9 +36,9 @@ fn handle_greet_entry(prev: EntryGreet) -> Next { } ``` -`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. +`AsPicker` implements `pick`, `pick_or`, and `pick_or_route` for all types convertible to `Vec<String>`. These functions semantically **pick** params from the string list and convert them into structured data. -Breaking down the example above: +For the code above: ```rust // Features: ["parser"] @@ -62,17 +62,17 @@ Its semantics are: @@@let name: String = prev.pick_or((), "World").unpack(); // ~~~~ ~~~~~~~ ~~ ~~~~~~~ ~~~~~~~~ -// | | | | |_ unpack to String +// | | | | |_ unpack as String // | | | |__________ default value "World" // | | |______________ pick the first positional arg (no flag) // | |______________________ pick or use default -// |___________________________ from previous input +// |___________________________ from the previous input @@@} ``` -## Parsing Flag Args +## Parsing Flag Arguments -If your program needs to parse flag args (e.g., `greet --name Alice`), do this: +If your program needs to parse flag arguments (e.g. `greet --name Alice`), do this: ```rust // Features: ["parser"] @@ -97,19 +97,19 @@ Its semantics: @@@let name: String = prev.pick_or(["--name", "-n"], "World").unpack(); // ~~~~ ~~~~~~~ ~~~~~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~ -// | | | | |_ unpack to String +// | | | | |_ unpack as String // | | | |__________ default value "World" -// | | |____________________________ pick arg after "--name" or "-n" +// | | |____________________________ pick the value after "--name" or "-n" // | |____________________________________ pick or use default -// |_________________________________________ from previous input +// |_________________________________________ from the 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. +You may have noticed that `Picker` calls `.unpack()` at the end of parsing. It converts the collected results into structured info. -For a single pick, `.unpack()` returns a single value; for multiple picks, it returns a tuple: +For a single pick, `.unpack()` returns the value directly; for multiple picks, it returns a tuple: ```rust // Features: ["parser"] @@ -129,16 +129,17 @@ fn handle_test_entry(prev: EntryTest) -> Next { ``` > [!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. +> `Picker` is sensitive to parse order, especially for positional args — it parses sequentially. If you need to parse positional args, make sure all **flag arguments** are 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. +As the saying goes: "never trust your users." To handle missing required params, type mismatches, etc., `pick_or_route` routes the chain to a dedicated error handler. -A simple example: +Here's a simple example: ```rust // Features: ["parser", "extra_macros"] +@@@use mingling::macros::buffer; @@@use mingling::macros::route; @@@dispatcher!("greet", CMDGreet => EntryGreet); @@@pack!(ResultName = String); @@ -150,29 +151,20 @@ fn handle_greet_entry(prev: EntryGreet) -> Next { .pick_or_route(["--name", "-n"], ErrorNoName::default()) .unpack(); - // Use route! macro to unpack pick_result + // Use route! macro to expand 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 +#[renderer(buffer)] +fn render_greet(result: ResultName) { + r_println!("Hello, {}!", *result); } ``` -With `pick_or_route`, the code gets a bit more complex: `.unpack()` no longer returns the value directly, but `Result<Value, Route>`. +With `pick_or_route`, the code becomes more involved: `.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: +However, **Mingling**'s `extra_macros` feature provides the `route!` macro for simplified expansion. It's not complex — it just reduces boilerplate: ```rust // Features: ["parser", "extra_macros"] @@ -204,7 +196,7 @@ let name = match pick_result { ## Post-processing Extracted Values -After picking user input, you can use `after` to process the value immediately: +After picking user input with `pick`, you can use `after` to process it immediately: ```rust // Features: ["parser"] @@ -215,7 +207,7 @@ After picking user input, you can use `after` to process the value immediately: fn handle_greet_entry(prev: EntryGreet) -> Next { let name = prev .pick_or(["--name", "-n"], "World") - // Format immediately after extracting --name + // Format immediately after picking --name .after(|name: String| { name.replace(['-', '_', '.'], " ") .to_lowercase() @@ -228,10 +220,11 @@ fn handle_greet_entry(prev: EntryGreet) -> Next { } ``` -Similarly, use `after_or_route` to handle format errors in input args: +Similarly, you can use `after_or_route` to handle input format errors: ```rust // Features: ["parser", "extra_macros"] +@@@use mingling::macros::buffer; @@@use mingling::macros::route; @@@dispatcher!("greet", CMDGreet => EntryGreet); @@@pack!(ResultName = String); @@ -254,35 +247,31 @@ fn handle_greet_entry(prev: EntryGreet) -> Next { ResultName::new(name).into() } -#[renderer] -fn render_name_too_long(prev: ErrorNameTooLong) -> RenderResult { - let mut r = RenderResult::new(); +#[renderer(buffer)] +fn render_name_too_long(prev: ErrorNameTooLong) { let len = *prev; - writeln!(r, "Error: name too long (length: {} > 32)", len).ok(); - r + r_println!("Error: name too long (length: {} > 32)", len); } -#[renderer] -fn render_name(prev: ResultName) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "Hello, {}!", *prev).ok(); - r +#[renderer(buffer)] +fn render_name(prev: ResultName) { + r_println!("Hello, {}!", *prev); } ``` ## Boolean Parsing -`Picker` can parse bools too, with two modes: implicit and explicit. +`Picker` can also parse booleans, in two modes: | 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 +- `.pick::<bool>(flag)` uses implicit mode: the flag being present means `true` +- `.pick::<Yes>(flag)` or `.pick::<True>(flag)` uses explicit mode -Implicit is fine for most cases, but for important confirmations, explicit logic is more semantic. +Implicit mode is generally sufficient, but for important confirmations, explicit logic is more idiomatic. ```rust // Features: ["parser"] @@ -302,7 +291,7 @@ fn handle_entry(prev: EntryTest) -> Next { ## Special Usage: `usize` Parsing -Mingling provides a special `usize` feature: parsing strings like `25G`, `32mib`, etc. +**Mingling** provides a special `usize` feature: parsing strings like `25G`, `32mib`, etc. ```rust // Features: ["parser"] @@ -315,12 +304,13 @@ fn parse_size() { } ``` -## Custom Parseable Types +## Custom Pickable Types -Implement the `Pickable` trait to make your type parseable by `Picker` — this is where Picker's extensibility comes from. +You can make your types pickable by `Picker` using the `Pickable` trait — this is where `Picker`'s extensibility comes from. ```rust // Features: ["parser"] +@@@use mingling::macros::buffer; @@@use mingling::parser::{Pickable, Argument}; @@@use mingling::Flag; #[derive(Default, Clone)] @@ -348,12 +338,9 @@ fn handle_connect_entry(prev: EntryConnect) -> Next { ResultConnected::new(address).into() } -#[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 +#[renderer(buffer)] +fn render_connected(addr: ResultConnected) { + r_println!("Connected: IP: {} PORT: {}", addr.ip, addr.port); } ``` @@ -366,10 +353,11 @@ 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`: +To make an enum `Pickable`, just implement `EnumTag` on it, then implement `PickableEnum`: ```rust // Features: ["parser"] +@@@use mingling::macros::buffer; @@@use mingling::parser::PickableEnum; @@@use mingling::EnumTag; #[derive(Debug, Default, EnumTag)] @@ -390,15 +378,13 @@ fn handle_eat_entry(prev: EntryEat) -> Next { ResultFruit::new(fruit).into() } -#[renderer] -fn render_fruit(prev: ResultFruit) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "Picked fruit: {:?}", *prev).ok(); - r +#[renderer(buffer)] +fn render_fruit(prev: ResultFruit) { + r_println!("Picked fruit: {:?}", *prev); } ``` -That covers all the features of `Picker`. +That covers all the usages of `Picker`. <p align="center" style="font-size: 0.85em; color: gray;"> Written by @Weicao-CatilGrass diff --git a/docs/pages/7-argument-parse-clap.md b/docs/pages/7-argument-parse-clap.md index 86fce35..dcebd65 100644 --- a/docs/pages/7-argument-parse-clap.md +++ b/docs/pages/7-argument-parse-clap.md @@ -25,6 +25,7 @@ Add `#[dispatcher_clap]` on a `clap::Parser` struct to auto-generate a Dispatche // Dependencies: // clap = "4" @@@ use mingling::macros::dispatcher_clap; +@@@ use mingling::macros::buffer; #[derive(Default, clap::Parser, Grouped)] #[dispatcher_clap("greet", CMDGreet, help = true, error = ErrorGreetParsed)] pub struct EntryGreet { @@ -34,23 +35,19 @@ pub struct EntryGreet { repeat: i32, } -#[renderer] -fn render_greet(greet: EntryGreet) -> RenderResult { - let mut r = RenderResult::new(); +#[renderer(buffer)] +fn render_greet(greet: EntryGreet) { let count = greet.repeat.max(0) as usize; - write!(r, "Hello, ").ok(); + r_print!("Hello, "); for _ in 0..count { - write!(r, "{} ", greet.name).ok(); + r_print!("{} ", greet.name); } - writeln!(r, "!").ok(); - r + r_println!("!"); } -#[renderer] -fn render_greet_parse_failed(err: ErrorGreetParsed) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "{}", *err).ok(); - r +#[renderer(buffer)] +fn render_greet_parse_failed(err: ErrorGreetParsed) { + r_println!("{}", *err); } ``` @@ -62,6 +59,7 @@ If you need `--help` support, register `BasicProgramSetup` in main and set the c // Features: ["clap"] // Dependencies: // clap = "4" +@@@use mingling::macros::buffer; @@@use mingling::setup::BasicProgramSetup; @@@use mingling::macros::dispatcher_clap; @@@#[derive(Default, clap::Parser, Grouped)] @@ -69,11 +67,9 @@ If you need `--help` support, register `BasicProgramSetup` in main and set the c @@@pub struct EntryGreet { @@@ name: String, @@@} -@@@#[renderer] -@@@fn render_greet(greet: EntryGreet) -> RenderResult { -@@@ let mut r = RenderResult::new(); -@@@ write!(r, "Hello, {}!", greet.name).ok(); -@@@ r +@@@#[renderer(buffer)] +@@@fn render_greet(greet: EntryGreet) { +@@@ r_println!("Hello, {}!", greet.name); @@@} fn main() { let mut program = ThisProgram::new(); diff --git a/docs/pages/9-error-handling.md b/docs/pages/9-error-handling.md index 3e004f2..f6e05e3 100644 --- a/docs/pages/9-error-handling.md +++ b/docs/pages/9-error-handling.md @@ -38,23 +38,20 @@ fn handle_greet(args: EntryGreet) -> Next { Then write separate Renderers: ```rust +@@@use mingling::macros::buffer; @@@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(buffer)] +fn render_greet(result: ResultGreeting) { + r_println!("Hello, {}!", *result); } -#[renderer] -fn render_error_name_empty(err: ErrorNameEmpty) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "Error: {}", *err).ok(); - r +#[renderer(buffer)] +fn render_error_name_empty(err: ErrorNameEmpty) { + r_println!("Error: {}", *err); } ``` @@ -63,6 +60,7 @@ Each Renderer does its own job; what the user sees depends on what the Chain ret ## Complete Example ```rust +@@@use mingling::macros::buffer; dispatcher!("greet", CMDGreet => EntryGreet); pack!(ResultGreeting = String); @@ -78,18 +76,14 @@ fn handle_greet(args: EntryGreet) -> Next { } } -#[renderer] -fn render_greeting(result: ResultGreeting) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "Hello, {}!", *result).ok(); - r +#[renderer(buffer)] +fn render_greet(result: ResultGreeting) { + r_println!("Hello, {}!", *result); } -#[renderer] -fn render_error_name_empty(err: ErrorNameEmpty) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "Error: {}", *err).ok(); - r +#[renderer(buffer)] +fn render_error_name_empty(err: ErrorNameEmpty) { + r_println!("Error: {}", *err); } fn main() { diff --git a/docs/pages/advanced/2-structural-renderer.md b/docs/pages/advanced/2-structural-renderer.md index 910b197..b54af22 100644 --- a/docs/pages/advanced/2-structural-renderer.md +++ b/docs/pages/advanced/2-structural-renderer.md @@ -27,6 +27,7 @@ After enabling `StructuralRendererSetup`, use `pack_structural!` instead of `pac // Features: ["structural_renderer"] // Dependencies: // serde = "1" +@@@use mingling::macros::buffer; @@@use mingling::setup::StructuralRendererSetup; @@@dispatcher!("render", CMDRender => EntryRender); @@ -40,11 +41,9 @@ fn handle_render(args: EntryRender) -> Next { ResultInfo::new((name, age)).into() } -#[renderer] -fn render_info(r: ResultInfo) -> RenderResult { - let mut result = RenderResult::new(); - writeln!(result, "{:?}", *r).ok(); - result +#[renderer(buffer)] +fn render_info(r: ResultInfo) { + r_println!("{:?}", *r); } ``` @@ -68,6 +67,7 @@ The default output from `pack_structural!` includes an `inner` field. For full c // Features: ["structural_renderer"] // Dependencies: // serde = "1" +@@@use mingling::macros::buffer; @@@use mingling::prelude::*; @@@use mingling::setup::StructuralRendererSetup; @@@use mingling::StructuralData; @@ -87,11 +87,9 @@ fn handle_render(args: EntryRender) -> Next { 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 +#[renderer(buffer)] +fn render_info(info: Info) { + r_println!("{} is {} years old", info.name, info.age); } @@@ @@@fn main() { diff --git a/docs/pages/other/naming_rule.md b/docs/pages/other/naming_rule.md index 089a711..1175ae0 100644 --- a/docs/pages/other/naming_rule.md +++ b/docs/pages/other/naming_rule.md @@ -166,6 +166,7 @@ fn handle_remote_add(args: EntryRemoteAdd, cwd: &ResCurrentDir, db: &mut ResData ## Complete Example ```rust +@@@use mingling::macros::buffer; @@@ #[derive(Default, Clone)] @@@ struct ResDatabase { } @@@ impl ResDatabase { fn has_remote(&self, remote: &String) -> bool { true } } @@ -192,21 +193,17 @@ fn handle_state_operation_remotes(state: StateOperationRemotes, db: &ResDatabase } // Result rendering -#[renderer] -fn render_remote_added(result: ResultRemoteAdded) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "Remote added: {}", result.inner).ok(); - r + +#[renderer(buffer)] +fn render_remote_added(result: ResultRemoteAdded) { + r_println!("Remote added: {}", result.inner); } // Error rendering -#[renderer] -fn render_error_repository_not_found(err: ErrorRepositoryNotFound) -> RenderResult { - let mut r = RenderResult::new(); - writeln!(r, "Error: remote '{}' not found", err.inner).ok(); - r +#[renderer(buffer)] +fn render_error_repository_not_found(err: ErrorRepositoryNotFound) { + r_println!("Error: remote '{}' not found", err.inner); } - ``` <p align="center" style="font-size: 0.85em; color: gray;"> diff --git a/docs/res/icon3.png b/docs/res/icon3.png Binary files differnew file mode 100644 index 0000000..04ea5e9 --- /dev/null +++ b/docs/res/icon3.png diff --git a/docs/res/icon3_1024.png b/docs/res/icon3_1024.png Binary files differnew file mode 100644 index 0000000..868f8ad --- /dev/null +++ b/docs/res/icon3_1024.png diff --git a/examples/example-argument-picker/Cargo.lock b/examples/example-argument-picker/Cargo.lock new file mode 100644 index 0000000..9a9baa4 --- /dev/null +++ b/examples/example-argument-picker/Cargo.lock @@ -0,0 +1,94 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arg-picker" +version = "0.1.0" +dependencies = [ + "arg-picker-macros", + "just_fmt", +] + +[[package]] +name = "arg-picker-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "example-argument-picker" +version = "0.1.0" +dependencies = [ + "mingling", +] + +[[package]] +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 = [ + "arg-picker", + "mingling_core", + "mingling_macros", +] + +[[package]] +name = "mingling_core" +version = "0.3.0" +dependencies = [ + "just_fmt", +] + +[[package]] +name = "mingling_macros" +version = "0.3.0" +dependencies = [ + "just_fmt", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +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 = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/examples/example-argument-picker/Cargo.toml b/examples/example-argument-picker/Cargo.toml new file mode 100644 index 0000000..686b95b --- /dev/null +++ b/examples/example-argument-picker/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "example-argument-picker" +version = "0.1.0" +edition = "2024" + +[dependencies.mingling] +path = "../../mingling" + +# Enable `picker` features +features = ["picker", "extra_macros"] + +[workspace] diff --git a/examples/example-argument-picker/page.toml b/examples/example-argument-picker/page.toml new file mode 100644 index 0000000..563bccc --- /dev/null +++ b/examples/example-argument-picker/page.toml @@ -0,0 +1,10 @@ +[example] +id = "example-argument-picker" +name = "Argument Picker" +icon = "📋" +category = "parsing" +desc = """ +Demonstrates how to use Mingling's `picker` feature and `Picker` to extract typed arguments from the command line. +""" +tags = ["arg-picker", "SinglePickable"] +files = ["src/main.rs", "Cargo.toml"] diff --git a/examples/example-argument-picker/src/main.rs b/examples/example-argument-picker/src/main.rs new file mode 100644 index 0000000..7fcc5db --- /dev/null +++ b/examples/example-argument-picker/src/main.rs @@ -0,0 +1,225 @@ +//! Example Argument Picker +//! +//! > Demonstrates how to use Mingling's `picker` feature and `Picker` to extract typed arguments from the command line. +//! +//! Run: +//! ```bash +//! cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 1 + 1 +//! cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 7 * 7 +//! cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc +//! cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 1 +//! cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 1 + +//! cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 4 / 3 +//! cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 4 / 3 --round +//! ``` +//! +//! Output: +//! ```plaintext +//! Result: 2 +//! Result: 49 +//! Error: First number (number_a) was not provided. +//! Error: Operator was not provided. +//! Error: Second number (number_b) was not provided. +//! Result: 1.3333334 +//! Result: 1 +//! ``` + +use mingling::{ + consts::REMAINS, + macros::route, + picker::{ + IntoPicker, PickerArgResult, SinglePickable, + parselib::{ParserStyle, UNIX_STYLE}, + value::Flag, + }, + prelude::*, +}; + +// --------- IMPORTANT --------- +// Use picker::BasicProgramSetup instead of the original BasicProgramSetup +// It uses arg-picker to rewrite the logic of the original BasicProgramSetup +use mingling::setup::picker::BasicProgramSetup; + +// --------- IMPORTANT --------- + +dispatcher!("calc", CMDCalculate => EntryCalculate); + +pack_err!(ErrorNumberANotProvided); +pack_err!(ErrorNumberBNotProvided); +pack_err!(ErrorNumberOperatorNotProvided); +pack_err!(ErrorDivisionByZero); + +pack!(StateAdd = (f32, f32)); +pack!(StateSubtract = (f32, f32)); +pack!(StateMultiply = (f32, f32)); +pack!(StateDivide = (f32, f32)); + +pack!(ResultNumber = f32); + +#[derive(Grouped)] +struct StateCalculate { + number_a: f32, + operator: Operator, + number_b: f32, +} + +#[derive(Debug, PartialEq, Eq)] +enum Operator { + Plus, + Dash, + Slash, + Star, +} + +// --------- IMPORTANT --------- +// Define SinglePickable for type Operator +// This allows the type to be picked as an argument +impl SinglePickable for Operator { + fn pick_single(str: Option<&str>) -> PickerArgResult<Self> { + let Some(str) = str else { + return PickerArgResult::NotFound; + }; + let op = match str.chars().next() { + Some('+') => Operator::Plus, + Some('-') => Operator::Dash, + Some('*') => Operator::Star, + Some('/') => Operator::Slash, + _ => return PickerArgResult::NotFound, + }; + PickerArgResult::Parsed(op) + } +} +// --------- IMPORTANT --------- + +#[derive(Default, Clone)] +struct ResNumberDisplaySetting { + round: bool, +} + +fn main() { + let mut program = ThisProgram::new(); + + // Use ParserStyle to manage the arg-picker theme + ParserStyle::set_global_style(&UNIX_STYLE); + + // Enable picker::BasicProgramSetup + program.with_setup(BasicProgramSetup); + + // --------- IMPORTANT --------- + // Pre-process global arguments before executing commands + let (round, args) = program + .take_args() + // Use arg![round: Flag] to indicate the `--round` | `-R` flag + // | + // vvvvvvvvvvvvvvvv + .pick(&arg![round: Flag, 'R']) + // Use REMAINS to extract remaining arguments + // | + // vvvvvvvv + .pick(&REMAINS) + // Since Flag and REMAINS will not fail to parse, + // we can safely unwrap here + .unwrap(); + program.replace_args(args.into()); + + program.with_resource(ResNumberDisplaySetting { round: *round }); + // --------- IMPORTANT --------- + + program.with_dispatcher(CMDCalculate); + program.exec_and_exit(); +} + +#[chain] +fn handle_calc(args: EntryCalculate) -> Next { + // --------- IMPORTANT --------- + let (number_a, operator, number_b) = route!( + // Use the arg! macro to define a positional argument of type f32 + // | + // vvvvvvvvvv + args.pick_or_route(&arg![f32], || ErrorNumberANotProvided::default().to_chain()) + .pick_or_route(&arg![Operator], || { + ErrorNumberOperatorNotProvided::default().to_chain() + }) // Returns a routable type when not found or fails to parse + // | + // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv + .pick_or_route(&arg![f32], || ErrorNumberBNotProvided::default().to_chain()) + // Use `to_result` to parse arguments + // and convert to Result<(Tuple, ...), Route> type + .to_result() + ); + // --------- IMPORTANT --------- + + if operator == Operator::Slash && number_b == 0. { + return ErrorDivisionByZero::default().to_chain(); + } + + StateCalculate { + number_a, + operator, + number_b, + } + .to_chain() +} + +#[chain] +fn handle_state_calculate(state: StateCalculate) -> Next { + match (state.operator, state.number_a, state.number_b) { + (Operator::Plus, a, b) => StateAdd::new((a, b)).to_chain(), + (Operator::Dash, a, b) => StateSubtract::new((a, b)).to_chain(), + (Operator::Slash, a, b) => StateDivide::new((a, b)).to_chain(), + (Operator::Star, a, b) => StateMultiply::new((a, b)).to_chain(), + } +} + +#[chain] +fn handle_state_add(state_add: StateAdd) -> ResultNumber { + let (a, b) = state_add.inner; + ResultNumber::new(a + b) +} + +#[chain] +fn handle_state_subtract(state_subtract: StateSubtract) -> ResultNumber { + let (a, b) = state_subtract.inner; + ResultNumber::new(a - b) +} + +#[chain] +fn handle_state_multiply(state_multiply: StateMultiply) -> ResultNumber { + let (a, b) = state_multiply.inner; + ResultNumber::new(a * b) +} + +#[chain] +fn handle_state_divide(state_divide: StateDivide) -> ResultNumber { + let (a, b) = state_divide.inner; + ResultNumber::new(a / b) +} + +#[renderer] +fn render_result_number(result: ResultNumber, setting: &ResNumberDisplaySetting) -> String { + let round = setting.round; + let result = if round { result.round() } else { result.inner }; + format!("Result: {}", result) +} + +#[renderer] +fn render_error_division_by_zero(_: ErrorDivisionByZero) -> String { + "Error: Division by zero is not allowed!".to_string() +} + +#[renderer] +fn render_error_number_a_not_provided(_: ErrorNumberANotProvided) -> String { + "Error: First number (number_a) was not provided.".to_string() +} + +#[renderer] +fn render_error_number_b_not_provided(_: ErrorNumberBNotProvided) -> String { + "Error: Second number (number_b) was not provided.".to_string() +} + +#[renderer] +fn render_error_number_operator_not_provided(_: ErrorNumberOperatorNotProvided) -> String { + "Error: Operator was not provided.".to_string() +} + +gen_program!(); diff --git a/examples/test-examples.toml b/examples/test-examples.toml index d2fe602..e450919 100644 --- a/examples/test-examples.toml +++ b/examples/test-examples.toml @@ -277,3 +277,38 @@ expect.result = "Hello, Alice!" command = "hello" expect.exit-code = 0 expect.result = "Hello, World!" + +[[test.example-argument-picker]] +command = "calc 1 + 1" +expect.exit-code = 0 +expect.result = "Result: 2" + +[[test.example-argument-picker]] +command = "calc 7 * 7" +expect.exit-code = 0 +expect.result = "Result: 49" + +[[test.example-argument-picker]] +command = "calc" +expect.exit-code = 0 +expect.result = "Error: First number (number_a) was not provided." + +[[test.example-argument-picker]] +command = "calc 1" +expect.exit-code = 0 +expect.result = "Error: Operator was not provided." + +[[test.example-argument-picker]] +command = "calc 1 +" +expect.exit-code = 0 +expect.result = "Error: Second number (number_b) was not provided." + +[[test.example-argument-picker]] +command = "calc 4 / 3" +expect.exit-code = 0 +expect.result = "Result: 1.3333334" + +[[test.example-argument-picker]] +command = "calc 4 / 3 --round" +expect.exit-code = 0 +expect.result = "Result: 1" diff --git a/mingling/Cargo.toml b/mingling/Cargo.toml index adf900f..351384f 100644 --- a/mingling/Cargo.toml +++ b/mingling/Cargo.toml @@ -21,7 +21,9 @@ mingling = { path = ".", features = [ ] } [package.metadata.docs.rs] +rustdoc-args = ["--generate-link-to-definition"] features = [ + "docs_rs", "core", "macros", "builds", @@ -50,7 +52,7 @@ 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:arg-picker", "arg-picker/mingling_support"] +picker = ["mingling_core/picker", "dep:arg-picker", "arg-picker/mingling_support"] pathf = ["mingling_core/pathf", "mingling_macros/pathf"] structural_renderer = [ @@ -87,6 +89,9 @@ ron_serde_fmt = ["mingling_core/ron_serde_fmt"] extra_macros = ["mingling_macros/extra_macros"] +# Section only shown in docs.rs +docs_rs = [] + [dependencies] mingling_core = { workspace = true, optional = true } mingling_macros = { workspace = true, optional = true } diff --git a/mingling/src/constants.rs b/mingling/src/constants.rs index 65d02e8..75f1c73 100644 --- a/mingling/src/constants.rs +++ b/mingling/src/constants.rs @@ -4,5 +4,8 @@ mod picker; #[cfg(feature = "picker")] pub use picker::*; +#[cfg(feature = "picker")] +pub use arg_picker::consts::*; + mod exit_codes; pub use exit_codes::*; diff --git a/mingling/src/constants/picker.rs b/mingling/src/constants/picker.rs index f2a448b..d98dcab 100644 --- a/mingling/src/constants/picker.rs +++ b/mingling/src/constants/picker.rs @@ -1,20 +1,6 @@ +use arg_picker::{PickerArg, value::Flag}; use std::marker::PhantomData; -use arg_picker::{PickerArg, PickerArgs, value::Flag}; - -/// Remaining positional arguments (anything not consumed as an option). -/// - `full`: `[]` (empty — not triggered by any `--` prefix). -/// - `short`: (none) -/// - `positional`: `false` (this is a meta‑argument that collects everything left). -/// This constant is used internally to access any leftover arguments after -/// all defined flags/options have been processed. -pub const REMAINS: PickerArg<PickerArgs> = PickerArg::<PickerArgs> { - full: &[], - short: None, - positional: false, - internal_type: PhantomData, -}; - /// Help flag: display usage information. /// - `full`: `["help"]` /// - `short`: `'h'` diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs index fad287f..bdefb5a 100644 --- a/mingling/src/example_docs.rs +++ b/mingling/src/example_docs.rs @@ -124,6 +124,251 @@ /// } /// ``` pub mod example_argument_parse {} +/// Example Argument Picker +/// +/// > Demonstrates how to use Mingling's `picker` feature and `Picker` to extract typed arguments from the command line. +/// +/// Run: +/// ```bash +/// cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 1 + 1 +/// cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 7 * 7 +/// cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc +/// cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 1 +/// cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 1 + +/// cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 4 / 3 +/// cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 4 / 3 --round +/// ``` +/// +/// Output: +/// ```plaintext +/// Result: 2 +/// Result: 49 +/// Error: First number (number_a) was not provided. +/// Error: Operator was not provided. +/// Error: Second number (number_b) was not provided. +/// Result: 1.3333334 +/// Result: 1 +/// ``` +/// +/// Source code (./Cargo.toml) +/// ```toml +/// [package] +/// name = "example-argument-picker" +/// version = "0.1.0" +/// edition = "2024" +/// +/// [dependencies.mingling] +/// path = "../../mingling" +/// +/// # Enable `picker` features +/// features = ["picker", "extra_macros"] +/// +/// [workspace] +/// ``` +/// +/// Source code (./src/main.rs) +/// ```ignore +/// use mingling::{ +/// consts::REMAINS, +/// macros::route, +/// picker::{ +/// IntoPicker, PickerArgResult, SinglePickable, +/// parselib::{ParserStyle, UNIX_STYLE}, +/// value::Flag, +/// }, +/// prelude::*, +/// }; +/// +/// // --------- IMPORTANT --------- +/// // Use picker::BasicProgramSetup instead of the original BasicProgramSetup +/// // It uses arg-picker to rewrite the logic of the original BasicProgramSetup +/// use mingling::setup::picker::BasicProgramSetup; +/// +/// // --------- IMPORTANT --------- +/// +/// dispatcher!("calc", CMDCalculate => EntryCalculate); +/// +/// pack_err!(ErrorNumberANotProvided); +/// pack_err!(ErrorNumberBNotProvided); +/// pack_err!(ErrorNumberOperatorNotProvided); +/// pack_err!(ErrorDivisionByZero); +/// +/// pack!(StateAdd = (f32, f32)); +/// pack!(StateSubtract = (f32, f32)); +/// pack!(StateMultiply = (f32, f32)); +/// pack!(StateDivide = (f32, f32)); +/// +/// pack!(ResultNumber = f32); +/// +/// #[derive(Grouped)] +/// struct StateCalculate { +/// number_a: f32, +/// operator: Operator, +/// number_b: f32, +/// } +/// +/// #[derive(Debug, PartialEq, Eq)] +/// enum Operator { +/// Plus, +/// Dash, +/// Slash, +/// Star, +/// } +/// +/// // --------- IMPORTANT --------- +/// // Define SinglePickable for type Operator +/// // This allows the type to be picked as an argument +/// impl SinglePickable for Operator { +/// fn pick_single(str: Option<&str>) -> PickerArgResult<Self> { +/// let Some(str) = str else { +/// return PickerArgResult::NotFound; +/// }; +/// let op = match str.chars().next() { +/// Some('+') => Operator::Plus, +/// Some('-') => Operator::Dash, +/// Some('*') => Operator::Star, +/// Some('/') => Operator::Slash, +/// _ => return PickerArgResult::NotFound, +/// }; +/// PickerArgResult::Parsed(op) +/// } +/// } +/// // --------- IMPORTANT --------- +/// +/// #[derive(Default, Clone)] +/// struct ResNumberDisplaySetting { +/// round: bool, +/// } +/// +/// fn main() { +/// let mut program = ThisProgram::new(); +/// +/// // Use ParserStyle to manage the arg-picker theme +/// ParserStyle::set_global_style(&UNIX_STYLE); +/// +/// // Enable picker::BasicProgramSetup +/// program.with_setup(BasicProgramSetup); +/// +/// // --------- IMPORTANT --------- +/// // Pre-process global arguments before executing commands +/// let (round, args) = program +/// .take_args() +/// // Use arg![round: Flag] to indicate the `--round` | `-R` flag +/// // | +/// // vvvvvvvvvvvvvvvv +/// .pick(&arg![round: Flag, 'R']) +/// // Use REMAINS to extract remaining arguments +/// // | +/// // vvvvvvvv +/// .pick(&REMAINS) +/// // Since Flag and REMAINS will not fail to parse, +/// // we can safely unwrap here +/// .unwrap(); +/// program.replace_args(args.into()); +/// +/// program.with_resource(ResNumberDisplaySetting { round: *round }); +/// // --------- IMPORTANT --------- +/// +/// program.with_dispatcher(CMDCalculate); +/// program.exec_and_exit(); +/// } +/// +/// #[chain] +/// fn handle_calc(args: EntryCalculate) -> Next { +/// // --------- IMPORTANT --------- +/// let (number_a, operator, number_b) = route!( +/// // Use the arg! macro to define a positional argument of type f32 +/// // | +/// // vvvvvvvvvv +/// args.pick_or_route(&arg![f32], || ErrorNumberANotProvided::default().to_chain()) +/// .pick_or_route(&arg![Operator], || { +/// ErrorNumberOperatorNotProvided::default().to_chain() +/// }) // Returns a routable type when not found or fails to parse +/// // | +/// // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv +/// .pick_or_route(&arg![f32], || ErrorNumberBNotProvided::default().to_chain()) +/// // Use `to_result` to parse arguments +/// // and convert to Result<(Tuple, ...), Route> type +/// .to_result() +/// ); +/// // --------- IMPORTANT --------- +/// +/// if operator == Operator::Slash && number_b == 0. { +/// return ErrorDivisionByZero::default().to_chain(); +/// } +/// +/// StateCalculate { +/// number_a, +/// operator, +/// number_b, +/// } +/// .to_chain() +/// } +/// +/// #[chain] +/// fn handle_state_calculate(state: StateCalculate) -> Next { +/// match (state.operator, state.number_a, state.number_b) { +/// (Operator::Plus, a, b) => StateAdd::new((a, b)).to_chain(), +/// (Operator::Dash, a, b) => StateSubtract::new((a, b)).to_chain(), +/// (Operator::Slash, a, b) => StateDivide::new((a, b)).to_chain(), +/// (Operator::Star, a, b) => StateMultiply::new((a, b)).to_chain(), +/// } +/// } +/// +/// #[chain] +/// fn handle_state_add(state_add: StateAdd) -> ResultNumber { +/// let (a, b) = state_add.inner; +/// ResultNumber::new(a + b) +/// } +/// +/// #[chain] +/// fn handle_state_subtract(state_subtract: StateSubtract) -> ResultNumber { +/// let (a, b) = state_subtract.inner; +/// ResultNumber::new(a - b) +/// } +/// +/// #[chain] +/// fn handle_state_multiply(state_multiply: StateMultiply) -> ResultNumber { +/// let (a, b) = state_multiply.inner; +/// ResultNumber::new(a * b) +/// } +/// +/// #[chain] +/// fn handle_state_divide(state_divide: StateDivide) -> ResultNumber { +/// let (a, b) = state_divide.inner; +/// ResultNumber::new(a / b) +/// } +/// +/// #[renderer] +/// fn render_result_number(result: ResultNumber, setting: &ResNumberDisplaySetting) -> String { +/// let round = setting.round; +/// let result = if round { result.round() } else { result.inner }; +/// format!("Result: {}", result) +/// } +/// +/// #[renderer] +/// fn render_error_division_by_zero(_: ErrorDivisionByZero) -> String { +/// "Error: Division by zero is not allowed!".to_string() +/// } +/// +/// #[renderer] +/// fn render_error_number_a_not_provided(_: ErrorNumberANotProvided) -> String { +/// "Error: First number (number_a) was not provided.".to_string() +/// } +/// +/// #[renderer] +/// fn render_error_number_b_not_provided(_: ErrorNumberBNotProvided) -> String { +/// "Error: Second number (number_b) was not provided.".to_string() +/// } +/// +/// #[renderer] +/// fn render_error_number_operator_not_provided(_: ErrorNumberOperatorNotProvided) -> String { +/// "Error: Operator was not provided.".to_string() +/// } +/// +/// gen_program!(); +/// ``` +pub mod example_argument_picker {} /// Example Async Runtime Support /// /// > This example shows how to drive an async runtime using the `async` feature diff --git a/mingling/src/features.rs b/mingling/src/features.rs index cb474ae..78d6226 100644 --- a/mingling/src/features.rs +++ b/mingling/src/features.rs @@ -97,6 +97,17 @@ pub const MINGLING_DISPATCH_TREE: bool = false; #[cfg(feature = "dispatch_tree")] #[allow(unused)] pub const MINGLING_DISPATCH_TREE: bool = true; +/// Whether the `docs_rs` feature is enabled +/// Current: `disabled` +#[cfg(not(feature = "docs_rs"))] +#[allow(unused)] +pub const MINGLING_DOCS_RS: bool = false; + +/// Whether the `docs_rs` feature is enabled +/// Current: `enabled` +#[cfg(feature = "docs_rs")] +#[allow(unused)] +pub const MINGLING_DOCS_RS: bool = true; /// Whether the `extra_macros` feature is enabled /// Current: `disabled` #[cfg(not(feature = "extra_macros"))] diff --git a/mingling/src/lib.md b/mingling/src/lib.md index 03fa61d..4d20a8b 100644 --- a/mingling/src/lib.md +++ b/mingling/src/lib.md @@ -76,7 +76,7 @@ Command not found: [great] ## Examples `Mingling` provides detailed usage examples for your reference. -See [Examples](_mingling_examples/index.html) or [Helpdoc](https://mingling-rs.github.io/mingling/docs/examples.html) +See [Examples](EXAMPLES/index.html) or [Helpdoc](https://mingling-rs.github.io/mingling/docs/examples.html) ## About Features diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs index f4bc9dc..f5362d5 100644 --- a/mingling/src/lib.rs +++ b/mingling/src/lib.rs @@ -1,3 +1,8 @@ +#![doc(html_logo_url = "https://github.com/mingling-rs/mingling/raw/main/docs/res/icon3.png")] +#![doc( + html_favicon_url = "https://github.com/mingling-rs/mingling/raw/main/docs/res/favicon_small.png" +)] +#![deny(missing_docs)] #![doc = include_str!("lib.md")] #[cfg(feature = "core")] @@ -42,6 +47,8 @@ pub mod macros { /// New Parser provided by the `picker` feature #[cfg(feature = "picker")] pub use arg_picker::macros::*; + /// `#[buffer]` - Wraps a unit-returning function to produce a `RenderResult`. + pub use mingling_macros::buffer; /// `#[chain]` - Used to generate a struct implementing the `Chain` trait via a method pub use mingling_macros::chain; /// `#[completion(EntryType)]` - Used to generate completion entry @@ -91,6 +98,12 @@ pub mod macros { /// `#[program_setup]` - Used to generate program setup #[cfg(feature = "extra_macros")] pub use mingling_macros::program_setup; + /// `r_print!` - Prints text to a `RenderResult` buffer (without newline). + /// See the macro documentation for implicit vs. explicit buffer usage. + pub use mingling_macros::r_print; + /// `r_println!` - Prints text to a `RenderResult` buffer (with newline). + /// See the macro documentation for implicit vs. explicit buffer usage. + pub use mingling_macros::r_println; #[doc(hidden)] pub use mingling_macros::register_chain; #[doc(hidden)] @@ -106,6 +119,10 @@ pub mod macros { /// `route! { /* ... */ }` - Used to generate a route that either returns a successful result or early returns an error. #[cfg(feature = "extra_macros")] pub use mingling_macros::route; + /// `#[routeify]` - An extension attribute macro that transforms `expr?` into `route!(expr)`. + /// Can be used standalone or as a chain/renderer extension: `#[chain(routeify, ...)]`. + #[cfg(feature = "extra_macros")] + pub use mingling_macros::routeify; /// `suggest! { "hello", "bye" }` - Used to generate suggestions #[cfg(feature = "comp")] pub use mingling_macros::suggest; @@ -127,8 +144,9 @@ pub use mingling_macros::Grouped; pub use mingling_macros::StructuralData; /// Example projects for `Mingling`, for learning how to use `Mingling` -#[cfg(feature = "core")] -pub mod _mingling_examples { +#[cfg(all(feature = "core", feature = "docs_rs"))] +#[allow(nonstandard_style)] +pub mod EXAMPLES { pub use crate::example_docs::*; } @@ -171,12 +189,15 @@ pub mod res; /// use mingling::prelude::*; /// ``` pub mod prelude { - /// Re-export of the `Grouped` derive macro for grouping types. + /// Re-export of the `Grouped` trait #[cfg(feature = "core")] pub use crate::Grouped; /// Re-export of the `RenderResult` struct for outputting rendering result #[cfg(feature = "core")] pub use crate::RenderResult; + /// Re-export of the `Routable` trait + #[cfg(feature = "core")] + pub use crate::Routable; /// Re-export of the `chain` macro for defining a chain of commands. #[cfg(feature = "macros")] pub use crate::macros::chain; @@ -208,6 +229,12 @@ pub mod prelude { /// Like `pack!` but also marks the type for structured output #[cfg(all(feature = "macros", feature = "structural_renderer"))] pub use mingling_macros::pack_structural; + /// `r_print!` - Prints text to a `RenderResult` buffer (without newline). + /// See the macro documentation for implicit vs. explicit buffer usage. + pub use mingling_macros::r_print; + /// `r_println!` - Prints text to a `RenderResult` buffer (with newline). + /// See the macro documentation for implicit vs. explicit buffer usage. + pub use mingling_macros::r_println; /// Re-export of the `completion` macro for generating completion entries. #[cfg(all(feature = "macros", feature = "comp"))] @@ -222,8 +249,4 @@ pub mod prelude { #[cfg(feature = "picker")] pub use crate::picker::EntryPicker; - - /// Used to enable the `writeln!` macro for `RenderResult` - #[cfg(feature = "core")] - pub use std::io::Write; } diff --git a/mingling/src/picker/entry_picker.rs b/mingling/src/picker/entry_picker.rs index 7364c50..d9fb37c 100644 --- a/mingling/src/picker/entry_picker.rs +++ b/mingling/src/picker/entry_picker.rs @@ -35,7 +35,7 @@ pub trait EntryPicker<'a, This> { ) -> PickerPattern1<'a, Next, ChainProcess<This>> where Self: Sized, - Next: Pickable<'a> + Default + Sized, + Next: Pickable<'a> + Sized, { let picker = Self::to_picker(self); Picker::build_pattern1(picker.into_args(), arg.into(), None) @@ -60,7 +60,7 @@ pub trait EntryPicker<'a, This> { ) -> PickerPattern1<'a, Next, ChainProcess<This>> where Self: Sized, - Next: Pickable<'a> + Default + Sized, + Next: Pickable<'a> + Sized, F: FnMut() -> Next + 'static, { self.pick(arg).or(func) @@ -107,7 +107,7 @@ pub trait EntryPicker<'a, This> { ) -> PickerPattern1<'a, Next, ChainProcess<This>> where Self: Sized, - Next: Pickable<'a> + Default + Sized, + Next: Pickable<'a> + Sized, F: FnMut() -> ChainProcess<This> + 'static, { self.pick(arg).or_route(func) diff --git a/mingling/src/setups/repl_basic.rs b/mingling/src/setups/repl_basic.rs index 71a38d2..cf04372 100644 --- a/mingling/src/setups/repl_basic.rs +++ b/mingling/src/setups/repl_basic.rs @@ -19,7 +19,9 @@ where /// meaning only the last configured instance will take effect globally. /// Do not configure multiple prompts with different values — only one will be used. pub enum BasicREPLPromptSetup { + /// A static prompt string that is displayed before each REPL input. Prompt(String), + /// A function that returns a dynamic prompt string each time the REPL reads input. Func(fn() -> String), } diff --git a/mingling_cli/Cargo.lock b/mingling_cli/Cargo.lock new file mode 100644 index 0000000..378c81b --- /dev/null +++ b/mingling_cli/Cargo.lock @@ -0,0 +1,104 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arg-picker" +version = "0.1.0" +dependencies = [ + "arg-picker-macros", + "just_fmt", +] + +[[package]] +name = "arg-picker-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +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 = [ + "arg-picker", + "mingling_core", + "mingling_macros", +] + +[[package]] +name = "mingling-cli" +version = "0.3.0" +dependencies = [ + "mingling", +] + +[[package]] +name = "mingling_core" +version = "0.3.0" +dependencies = [ + "just_fmt", + "mingling_pathf", +] + +[[package]] +name = "mingling_macros" +version = "0.3.0" +dependencies = [ + "just_fmt", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "mingling_pathf" +version = "0.3.0" +dependencies = [ + "just_fmt", + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +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 = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/mingling_cli/Cargo.toml b/mingling_cli/Cargo.toml new file mode 100644 index 0000000..1cc45b6 --- /dev/null +++ b/mingling_cli/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "mingling-cli" +version = "0.3.0" +edition = "2024" +license = "MIT OR Apache-2.0" +repository = "https://github.com/mingling-rs/mingling/tree/main/mingling_cli" +authors = ["Weicao-CatilGrass"] +readme = "README.md" + +description = "Mingling's scaffolding tool for generating, analyzing, and modifying Mingling projects" +keywords = ["cli", "cli-framework", "command-line", "tools"] +categories = ["command-line-interface"] + +[dependencies.mingling] +path = "../mingling" +features = [ + "picker", + "pathf", + "dispatch_tree", + "extra_macros" +] + +[dependencies] + +[workspace] diff --git a/mingling_cli/src/main.rs b/mingling_cli/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/mingling_cli/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/mingling_core/Cargo.toml b/mingling_core/Cargo.toml index 85afd55..14140c6 100644 --- a/mingling_core/Cargo.toml +++ b/mingling_core/Cargo.toml @@ -15,6 +15,7 @@ nightly = [] default = [] async = [] builds = [] +picker = [] dispatch_tree = [] structural_renderer = ["dep:serde"] diff --git a/mingling_core/src/any.rs b/mingling_core/src/any.rs index e6b7406..ec29a1b 100644 --- a/mingling_core/src/any.rs +++ b/mingling_core/src/any.rs @@ -17,7 +17,18 @@ pub mod group; #[derive(Debug)] pub struct AnyOutput<G> { pub(crate) inner: Box<dyn std::any::Any + Send + 'static>, + + /// The [`TypeId`] of the concrete type stored in `inner`. + /// + /// This is set during construction and used for type-checking + /// in downcast, restore, and is methods. pub type_id: std::any::TypeId, + + /// The variant identifier returned by [`Grouped::member_id`] for the + /// concrete type stored in `inner`. + /// + /// This is used by the scheduler to dispatch on the correct enum + /// variant when routing the output. pub member_id: G, } @@ -106,7 +117,9 @@ impl<G> std::ops::DerefMut for AnyOutput<G> { /// - Returns `Ok((`[`AnyOutput`](./struct.AnyOutput.html)`, `[`NextProcess::Renderer`](./enum.NextProcess.html)`))` to render this type next and output to the terminal /// - Returns `Err(`[`ChainProcessError`](./error/enum.ChainProcessError.html)`]` to terminate the program directly pub enum ChainProcess<G> { + /// Indicates success, containing the output value and the next step to execute. Ok((AnyOutput<G>, NextProcess)), + /// Indicates a processing failure, containing the error that occurred. Err(ChainProcessError), } @@ -117,7 +130,9 @@ pub enum ChainProcess<G> { #[derive(Debug, PartialEq, Eq)] #[repr(u8)] pub enum NextProcess { + /// Continue execution to the next chain Chain, + /// Send output to renderer and end execution Renderer, } diff --git a/mingling_core/src/any/group.rs b/mingling_core/src/any/group.rs index 90ef9fc..2813ad5 100644 --- a/mingling_core/src/any/group.rs +++ b/mingling_core/src/any/group.rs @@ -11,26 +11,6 @@ where { /// Returns the specific enum value representing its ID within that enum fn member_id() -> Group; - - /// Converts the grouped item into a `ChainProcess` directed to the chain route. - /// - /// This wraps the item into an `AnyOutput` and routes it to the chain processing pipeline. - fn to_chain(self) -> ChainProcess<Group> - where - Self: Send, - { - AnyOutput::new(self).route_chain() - } - - /// Converts the grouped item into a `ChainProcess` directed to the render route. - /// - /// This wraps the item into an `AnyOutput` and routes it to the render processing pipeline. - fn to_render(self) -> ChainProcess<Group> - where - Self: Send, - { - AnyOutput::new(self).route_renderer() - } } impl<T, C> Routable<C> for T @@ -39,10 +19,10 @@ where T: Grouped<C> + Send, { fn to_chain(self) -> ChainProcess<C> { - T::to_chain(self) + AnyOutput::new(self).route_chain() } fn to_render(self) -> ChainProcess<C> { - T::to_render(self) + AnyOutput::new(self).route_renderer() } } diff --git a/mingling_core/src/asset/dispatcher.rs b/mingling_core/src/asset/dispatcher.rs index 8f04955..01c9ccf 100644 --- a/mingling_core/src/asset/dispatcher.rs +++ b/mingling_core/src/asset/dispatcher.rs @@ -32,22 +32,46 @@ where C: ProgramCollect<Enum = C>, { /// Adds a dispatcher to the program. - #[cfg(not(feature = "dispatch_tree"))] + #[cfg_attr( + feature = "dispatch_tree", + deprecated( + note = "When the `dispatch_tree` feature is enabled, the `dispatcher` field no longer exists inside Program. All types are collected at compile time by the `gen_program!()` macro, so the `with_dispatcher` function is no longer needed" + ) + )] pub fn with_dispatcher<Disp>(&mut self, dispatcher: Disp) where Disp: Dispatcher<C> + Send + Sync + 'static, { - self.dispatcher.push(Box::new(dispatcher)); + #[cfg(not(feature = "dispatch_tree"))] + { + self.dispatcher.push(Box::new(dispatcher)); + } + #[cfg(feature = "dispatch_tree")] + { + let _ = dispatcher; + } } /// Add some dispatchers to the program. - #[cfg(not(feature = "dispatch_tree"))] + #[cfg_attr( + feature = "dispatch_tree", + deprecated( + note = "When the `dispatch_tree` feature is enabled, the `dispatcher` field no longer exists inside Program. All types are collected at compile time by the `gen_program!()` macro, so the `with_dispatcher` function is no longer needed" + ) + )] pub fn with_dispatchers<D>(&mut self, dispatchers: D) where D: Into<Dispatchers<C>>, { - let dispatchers = dispatchers.into(); - self.dispatcher.extend(dispatchers.dispatcher); + #[cfg(not(feature = "dispatch_tree"))] + { + let dispatchers = dispatchers.into(); + self.dispatcher.extend(dispatchers.dispatcher); + } + #[cfg(feature = "dispatch_tree")] + { + let _ = dispatchers; + } } } diff --git a/mingling_core/src/comp/shell_ctx.rs b/mingling_core/src/comp/shell_ctx.rs index cfa4700..1fca325 100644 --- a/mingling_core/src/comp/shell_ctx.rs +++ b/mingling_core/src/comp/shell_ctx.rs @@ -1,3 +1,5 @@ +#![allow(deprecated)] + use std::collections::HashSet; use crate::{Flag, ShellFlag, Suggest}; @@ -115,6 +117,13 @@ impl ShellContext { /// // } /// } /// ``` + #[must_use] + #[cfg_attr( + feature = "picker", + deprecated( + note = "When using the `picker` feature, this method does not work under all ParserStyle settings" + ) + )] pub fn filling_argument_first(&self, flag: impl Into<Flag>) -> bool { let flag = flag.into(); if self.filling_argument(&flag) { @@ -153,6 +162,13 @@ impl ShellContext { /// // } /// } /// ``` + #[must_use] + #[cfg_attr( + feature = "picker", + deprecated( + note = "When using the `picker` feature, this method does not work under all ParserStyle settings" + ) + )] pub fn filling_argument(&self, flag: impl Into<Flag>) -> bool { for f in flag.into().iter() { if self.previous_word == **f { @@ -190,6 +206,12 @@ impl ShellContext { /// } /// ``` #[must_use] + #[cfg_attr( + feature = "picker", + deprecated( + note = "When using the `picker` feature, this method does not work under all ParserStyle settings" + ) + )] pub fn typing_argument(&self) -> bool { #[cfg(target_os = "windows")] { @@ -208,6 +230,12 @@ impl ShellContext { /// when the user has already typed certain flags. The method processes both /// regular suggestion sets and file completion suggestions differently. #[must_use] + #[cfg_attr( + feature = "picker", + deprecated( + note = "When using the `picker` feature, this method does not work under all ParserStyle settings" + ) + )] pub fn strip_typed_argument(&self, suggest: Suggest) -> Suggest { let typed = Self::get_typed_arguments(self); match suggest { @@ -225,6 +253,12 @@ impl ShellContext { /// which typically represent command-line flags or options. It returns a vector /// containing these flag strings, converted to owned `String` values. #[must_use] + #[cfg_attr( + feature = "picker", + deprecated( + note = "When using the `picker` feature, this method does not work under all ParserStyle settings" + ) + )] pub fn get_typed_arguments(&self) -> HashSet<String> { self.all_words .iter() diff --git a/mingling_core/src/comp/suggest.rs b/mingling_core/src/comp/suggest.rs index a81de64..99afc54 100644 --- a/mingling_core/src/comp/suggest.rs +++ b/mingling_core/src/comp/suggest.rs @@ -1,3 +1,5 @@ +#![allow(deprecated)] + use std::collections::BTreeSet; use crate::ShellContext; @@ -30,6 +32,12 @@ impl Suggest { /// Filters out already typed flag arguments from suggestion results. #[must_use] + #[cfg_attr( + feature = "picker", + deprecated( + note = "When using the `picker` feature, this method does not work under all ParserStyle settings" + ) + )] pub fn strip_typed_argument(self, ctx: &ShellContext) -> Self { ctx.strip_typed_argument(self) } diff --git a/mingling_core/src/lib.rs b/mingling_core/src/lib.rs index 4996b19..2cb24b9 100644 --- a/mingling_core/src/lib.rs +++ b/mingling_core/src/lib.rs @@ -8,6 +8,8 @@ //! //! Recommended to import [mingling](https://crates.io/crates/mingling) to use its features. +#![deny(missing_docs)] + mod any; mod asset; mod program; @@ -77,6 +79,10 @@ pub mod comp; #[cfg(feature = "comp")] pub use crate::comp::*; +/// Module for setting up a `Mingling` program. +/// +/// This module provides the [`ProgramSetup`] type, which allows users to configure +/// and initialize the program's execution environment. pub mod setup { pub use crate::program::setup::ProgramSetup; } diff --git a/mingling_core/src/program.rs b/mingling_core/src/program.rs index 0d409b7..11e1bbf 100644 --- a/mingling_core/src/program.rs +++ b/mingling_core/src/program.rs @@ -53,10 +53,30 @@ where #[cfg(not(feature = "dispatch_tree"))] pub(crate) dispatcher: Vec<Box<dyn Dispatcher<C> + Send + Sync>>, + /// Program stdout settings. + /// + /// This struct controls the program's output behavior, including whether + /// to output errors, render results, suppress panic messages, enable + /// verbose/quiet/debug modes, colored output, and progress indicators. + /// + /// # Convention-only fields + /// + /// Fields marked with convention only are not enforced by the framework; + /// they serve as a shared convention for applications to follow consistently. pub stdout_setting: ProgramStdoutSetting, + + /// User-defined context that can be accessed throughout the program. + /// + /// This field holds a user-defined context value that can be + /// accessed and modified at any point during program execution. It is + /// useful for passing shared state or configuration across different + /// parts of the program. pub user_context: ProgramUserContext, #[cfg(feature = "structural_renderer")] + /// Setting for the structural renderer. + /// + /// This field is only available when the `structural_renderer` feature is enabled. pub structural_renderer_name: StructuralRendererSetting, pub(crate) hooks: Vec<ProgramHook<C>>, diff --git a/mingling_core/src/program/collection.rs b/mingling_core/src/program/collection.rs index fe78979..14705ac 100644 --- a/mingling_core/src/program/collection.rs +++ b/mingling_core/src/program/collection.rs @@ -21,8 +21,16 @@ pub use mock::*; pub trait ProgramCollect { /// Enum type representing internal IDs for the program type Enum; + /// Error type when a dispatcher is not found for the given member type ErrorDispatcherNotFound: Grouped<Self::Enum>; + + /// Error type when a renderer is not found for the given member type ErrorRendererNotFound: Grouped<Self::Enum>; + + /// Result type for an empty chain result + /// + /// When the `extra_macros` feature is enabled, + /// you can use the `empty_result!()` macro to create this type ResultEmpty: Grouped<Self::Enum>; /// Use a prefix tree to quickly match arguments and dispatch to an Entry diff --git a/mingling_core/src/renderer/render_result.rs b/mingling_core/src/renderer/render_result.rs index e57a5b9..2c60fa4 100644 --- a/mingling_core/src/renderer/render_result.rs +++ b/mingling_core/src/renderer/render_result.rs @@ -8,6 +8,11 @@ use std::{ #[derive(Default, Debug, PartialEq)] pub struct RenderResult { render_text: String, + + /// The exit code to return from the rendering process. + /// + /// A value of `0` indicates success, while non-zero values indicate + /// various error conditions. pub exit_code: i32, } @@ -132,8 +137,8 @@ impl RenderResult { /// result.print(", world!"); /// assert_eq!(result.deref(), "Hello, world!"); /// ``` - pub fn print(&mut self, text: &str) { - self.render_text.push_str(text); + pub fn print(&mut self, text: impl AsRef<str>) { + self.render_text.push_str(text.as_ref()); } /// Appends the given text followed by a newline to the rendered content. @@ -149,8 +154,8 @@ impl RenderResult { /// result.println("Second line"); /// assert_eq!(result.deref(), "First line\nSecond line\n"); /// ``` - pub fn println(&mut self, text: &str) { - self.render_text.push_str(text); + pub fn println(&mut self, text: impl AsRef<str>) { + self.render_text.push_str(text.as_ref()); self.render_text.push('\n'); } diff --git a/mingling_core/src/renderer/structural/error.rs b/mingling_core/src/renderer/structural/error.rs index a7fbc75..63ded81 100644 --- a/mingling_core/src/renderer/structural/error.rs +++ b/mingling_core/src/renderer/structural/error.rs @@ -9,6 +9,7 @@ pub struct StructuralRendererSerializeError { } impl StructuralRendererSerializeError { + /// Creates a new `StructuralRendererSerializeError` with the given error message. #[must_use] pub fn new(error: String) -> Self { Self { error } diff --git a/mingling_macros/src/attr.rs b/mingling_macros/src/attr.rs new file mode 100644 index 0000000..e4cd826 --- /dev/null +++ b/mingling_macros/src/attr.rs @@ -0,0 +1,9 @@ +pub(crate) mod chain; +#[cfg(feature = "comp")] +pub(crate) mod completion; +#[cfg(feature = "clap")] +pub(crate) mod dispatcher_clap; +pub(crate) mod help; +#[cfg(feature = "extra_macros")] +pub(crate) mod program_setup; +pub(crate) mod renderer; diff --git a/mingling_macros/src/chain.rs b/mingling_macros/src/attr/chain.rs index ef31854..dbd87dd 100644 --- a/mingling_macros/src/chain.rs +++ b/mingling_macros/src/attr/chain.rs @@ -34,16 +34,14 @@ fn validate_return_type(sig: &Signature) -> Result<(), proc_macro2::TokenStream> /// Builds the `proc` function implementation inside the generated `Chain` impl. /// -/// The user's function body is inlined directly, and its result is converted -/// via `.into()` to `ChainProcess<ProgramType>`. -#[allow(unused_variables)] +/// Instead of inlining the user's body, the trait method calls the original +/// function by name, with resources injected from the application context. fn generate_proc_fn( + fn_name: &Ident, has_resources: bool, resources: &[ResourceInjection], program_type: &proc_macro2::TokenStream, previous_type: &TypePath, - prev_param: &Pat, - fn_body_stmts: &[syn::Stmt], is_async_fn: bool, is_unit_return: bool, origin_return_type: &proc_macro2::TokenStream, @@ -51,10 +49,52 @@ fn generate_proc_fn( let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), program_type); let mut_resources: Vec<_> = resources.iter().filter(|r| r.is_mut).collect(); + // Use a fixed parameter name `prev` for the trait method, regardless of + // the user's original parameter name (which may be `_` and cannot be + // referenced in expression position). + let fixed_prev: Pat = syn::parse_quote!(prev); + + // Build the call to the original function with resource arguments injected. + // The variable names come from the resource injection bindings + // (immut bindings are `let #name = …`, mut closures receive `|#name: &mut T|`), + // which match the original function's parameter names. + let resource_args: Vec<_> = resources + .iter() + .map(|res| { + let var_name = &res.var_name; + quote! { #var_name } + }) + .collect(); + + let fn_call = if has_resources { + let call = quote! { #fn_name(#fixed_prev, #(#resource_args),*) }; + if is_async_fn { + quote! { #call.await } + } else { + call + } + } else { + let call = quote! { #fn_name(#fixed_prev) }; + if is_async_fn { + quote! { #call.await } + } else { + call + } + }; + + // Convert the function call to a syn::Stmt so existing wrapping functions can use it + let fn_call_expr: syn::Expr = syn::parse_quote! { #fn_call }; + let fn_call_stmt = syn::Stmt::Expr(fn_call_expr, None); + let wrapped_body = if is_async_fn && !mut_resources.is_empty() { - wrap_body_with_mut_resources_async(fn_body_stmts, &mut_resources, program_type) + wrap_body_with_mut_resources_async(&[fn_call_stmt], &mut_resources, program_type) } else { - wrap_body_with_mut_resources(fn_body_stmts, &mut_resources, program_type, is_unit_return) + wrap_body_with_mut_resources( + &[fn_call_stmt], + &mut_resources, + program_type, + is_unit_return, + ) }; let proc_body = if is_unit_return { @@ -62,13 +102,13 @@ fn generate_proc_fn( quote! { #(#immut_resource_stmts)* #wrapped_body; - <crate::ResultEmpty as ::mingling::Grouped::<crate::ThisProgram>> + <crate::ResultEmpty as ::mingling::Routable::<crate::ThisProgram>> ::to_chain(crate::ResultEmpty) } } else { quote! { #wrapped_body; - <crate::ResultEmpty as ::mingling::Grouped::<crate::ThisProgram>> + <crate::ResultEmpty as ::mingling::Routable::<crate::ThisProgram>> ::to_chain(crate::ResultEmpty) } }; @@ -82,11 +122,6 @@ fn generate_proc_fn( } else { quote! { #wrapped_body } }; - // Convert the body result to `ChainProcess` using the user-declared - // return type as the source type for a fully-qualified `Into` call. - // This works for both: - // - `-> Next` / `-> ChainProcess`: identity `From<T> for T` - // - `-> PackType`: `Into<ChainProcess>` from pack!/derive quote! { let __chain_result = { #body }; <#origin_return_type as ::std::convert::Into< @@ -98,7 +133,7 @@ fn generate_proc_fn( #[cfg(feature = "async")] { quote! { - async fn proc(#prev_param: #previous_type) -> ::mingling::ChainProcess<#program_type> { + async fn proc(#fixed_prev: #previous_type) -> ::mingling::ChainProcess<#program_type> { #proc_body } } @@ -107,7 +142,7 @@ fn generate_proc_fn( #[cfg(not(feature = "async"))] { quote! { - fn proc(#prev_param: #previous_type) -> ::mingling::ChainProcess<#program_type> { + fn proc(#fixed_prev: #previous_type) -> ::mingling::ChainProcess<#program_type> { #proc_body } } @@ -158,7 +193,7 @@ fn reject_async(sig: &Signature) -> Result<(), proc_macro2::TokenStream> { Ok(()) } -pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream { +pub(crate) fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream { // Reject non-empty attribute arguments; #[chain] must be bare if !attr.is_empty() { return syn::Error::new( @@ -192,13 +227,12 @@ pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream { } // Extract the previous type, parameter name, and resource injection params - let (prev_param, previous_type, resources) = match extract_args_info(&input_fn.sig) { + let (_, previous_type, resources) = match extract_args_info(&input_fn.sig) { Ok(info) => info, Err(e) => return e.to_compile_error().into(), }; // Prepare building blocks - let fn_body = &input_fn.block; let mut fn_attrs = input_fn.attrs.clone(); fn_attrs.retain(|attr| !attr.path().is_ident("chain")); let vis = &input_fn.vis; @@ -223,12 +257,11 @@ pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream { // Generate the `proc` function for the Chain impl let proc_fn = generate_proc_fn( + fn_name, has_resources, &resources, &program_type, &previous_type, - &prev_param, - &fn_body.stmts, #[cfg(feature = "async")] is_async_fn, #[cfg(not(feature = "async"))] @@ -237,11 +270,9 @@ pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream { &origin_return_type, ); - // Preserve the original function untouched, with dead_code allowed - // since it may only be called through the Chain trait dispatch. + // Preserve the original function untouched // Note: do NOT add `#vis` here — `input_fn` (ItemFn) already contains its own visibility. let original_fn = quote! { - #[allow(dead_code)] #(#fn_attrs)* #input_fn }; @@ -263,7 +294,10 @@ pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream { } /// Builds a match arm for chain mapping -pub fn build_chain_arm(struct_name: &Ident, previous_type: &TypePath) -> proc_macro2::TokenStream { +pub(crate) fn build_chain_arm( + struct_name: &Ident, + previous_type: &TypePath, +) -> proc_macro2::TokenStream { let enum_variant = &previous_type.path.segments.last().unwrap().ident; quote! { #struct_name => #enum_variant, @@ -271,14 +305,14 @@ pub fn build_chain_arm(struct_name: &Ident, previous_type: &TypePath) -> proc_ma } /// Builds a match arm for chain existence check -pub fn build_chain_exist_arm(previous_type: &TypePath) -> proc_macro2::TokenStream { +pub(crate) fn build_chain_exist_arm(previous_type: &TypePath) -> proc_macro2::TokenStream { let enum_variant = &previous_type.path.segments.last().unwrap().ident; quote! { Self::#enum_variant => true, } } -pub fn register_chain(input: TokenStream) -> TokenStream { +pub(crate) fn register_chain(input: TokenStream) -> TokenStream { // Parse the input as a comma-separated list of arguments let input_parsed = syn::parse_macro_input!(input with syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>::parse_terminated); diff --git a/mingling_macros/src/completion.rs b/mingling_macros/src/attr/completion.rs index ae01462..3ced091 100644 --- a/mingling_macros/src/completion.rs +++ b/mingling_macros/src/attr/completion.rs @@ -5,7 +5,7 @@ use syn::spanned::Spanned; use syn::{FnArg, Ident, ItemFn, Pat, PatType, Type, TypePath, parse_macro_input}; #[cfg(feature = "comp")] -pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream { +pub(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream { // Parse the attribute arguments such as HelloEntry or crate::EntryFine from #[completion(crate::EntryFine)] use crate::get_global_set; let previous_type_path: TypePath = if attr.is_empty() { @@ -47,11 +47,8 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream { // Extract the first param pattern and type for the ctx parameter let first_arg = &inputs[0]; - let (ctx_pat, _ctx_type) = match first_arg { - FnArg::Typed(PatType { pat, ty, .. }) => { - let param_pat = (**pat).clone(); - (param_pat, (**ty).clone()) - } + let _ctx_type = match first_arg { + FnArg::Typed(PatType { ty, .. }) => (**ty).clone(), FnArg::Receiver(_) => { return syn::Error::new( first_arg.span(), @@ -61,6 +58,7 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream { .into(); } }; + let fixed_ctx: Pat = syn::parse_quote!(ctx); // Extract resources from params 2 through N, skipping ctx let resources = match extract_resources_from_args(sig, 1) { @@ -70,7 +68,6 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream { // Get the function body let fn_body = &input_fn.block; - let fn_body_stmts = &fn_body.stmts; // Get function attributes excluding the completion attribute let mut fn_attrs = input_fn.attrs.clone(); @@ -96,12 +93,26 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream { // Generate immutable resource bindings let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), &program_type); - // Build the comp method body with resource injection - // Use modify_res for mutable resources same pattern as renderer.rs - let wrapped_body = if mut_resources.is_empty() { - quote! { #(#fn_body_stmts)* } + // Build the call to the original function with resource arguments injected + let resource_args: Vec<_> = resources + .iter() + .map(|res| { + let var_name = &res.var_name; + quote! { #var_name } + }) + .collect(); + + let fn_call = if has_resources { + quote! { #fn_name(#fixed_ctx, #(#resource_args),*) } + } else { + quote! { #fn_name(#fixed_ctx) } + }; + + // Wrap the function call with modify_res for mutable resources + let inner_call = if mut_resources.is_empty() { + fn_call } else { - let mut wrapped = quote! { #(#fn_body_stmts)* }; + let mut wrapped = fn_call; for res in mut_resources.iter().rev() { let var_name = &res.var_name; let inner_type = &res.inner_type; @@ -117,10 +128,10 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream { let comp_body = if has_resources { quote! { #(#immut_resource_stmts)* - #wrapped_body + #inner_call } } else { - quote! { #(#fn_body_stmts)* } + quote! { #inner_call } }; // Generate the struct and implementation @@ -135,7 +146,7 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream { impl ::mingling::Completion for #struct_name { type Previous = #previous_type_path; - fn comp(#ctx_pat: &::mingling::ShellContext) #output { + fn comp(#fixed_ctx: &::mingling::ShellContext) #output { #comp_body } } diff --git a/mingling_macros/src/dispatcher_clap.rs b/mingling_macros/src/attr/dispatcher_clap.rs index 0945e31..40f7d47 100644 --- a/mingling_macros/src/dispatcher_clap.rs +++ b/mingling_macros/src/attr/dispatcher_clap.rs @@ -93,7 +93,7 @@ impl Parse for DispatcherClapInput { } #[cfg(feature = "clap")] -pub fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> TokenStream { +pub(crate) fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> TokenStream { let attr_input = parse_macro_input!(attr as DispatcherClapInput); let input_struct = parse_macro_input!(item as ItemStruct); let struct_name = &input_struct.ident; @@ -108,23 +108,23 @@ pub fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> TokenStream let begin_body = if let Some(ref error_struct) = options.error_struct { quote! { if ::mingling::this::<#program_path>().user_context.help { - return #struct_name::default().to_chain(); + return ::mingling::Routable::<#program_path>::to_chain(#struct_name::default()); } match <#struct_name as ::clap::Parser>::try_parse_from(clap_args) { - Ok(parsed) => parsed.to_chain(), + Ok(parsed) => ::mingling::Routable::<#program_path>::to_chain(parsed), Err(e) => { - return #error_struct::new(format!("{}", e.render().ansi())).to_render() + return ::mingling::Routable::<#program_path>::to_render(#error_struct::new(format!("{}", e.render().ansi()))) }, } } } else { quote! { if ::mingling::this::<#program_path>().user_context.help { - return #struct_name::default().to_chain(); + return ::mingling::Routable::<#program_path>::to_chain(#struct_name::default()); } let parsed = <#struct_name as ::clap::Parser>::try_parse_from(clap_args) .unwrap_or_else(|e| e.exit()); - parsed.to_chain() + ::mingling::Routable::<#program_path>::to_chain(parsed) } }; @@ -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) -> ::mingling::RenderResult { + pub(crate) fn #help_fn_name(_prev: #struct_name) -> ::mingling::RenderResult { use std::io::Write; use clap::ColorChoice; @@ -189,7 +189,7 @@ pub fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> TokenStream // Generate the dispatcher struct #[doc(hidden)] #[derive(Default)] - pub struct #dispatcher_struct; + pub(crate) struct #dispatcher_struct; impl ::mingling::Dispatcher<#program_path> for #dispatcher_struct { fn node(&self) -> ::mingling::Node { diff --git a/mingling_macros/src/help.rs b/mingling_macros/src/attr/help.rs index 6e0d12e..aa7bc88 100644 --- a/mingling_macros/src/help.rs +++ b/mingling_macros/src/attr/help.rs @@ -1,7 +1,7 @@ use proc_macro::TokenStream; use quote::{ToTokens, quote}; use syn::spanned::Spanned; -use syn::{Ident, ItemFn, ReturnType, Signature, TypePath, parse_macro_input}; +use syn::{Ident, ItemFn, Pat, ReturnType, Signature, TypePath, parse_macro_input}; use crate::get_global_set; use crate::res_injection::{extract_args_info, generate_immut_resource_bindings}; @@ -14,7 +14,7 @@ fn extract_user_return_type(sig: &Signature) -> Option<proc_macro2::TokenStream> } } -pub fn help_attr(item: TokenStream) -> TokenStream { +pub(crate) fn help_attr(item: TokenStream) -> TokenStream { // Parse the function item let input_fn = parse_macro_input!(item as ItemFn); @@ -25,8 +25,8 @@ pub fn help_attr(item: TokenStream) -> TokenStream { .into(); } - // Extract the entry type, parameter name, and resource injection params - let (prev_param, entry_type, resources) = match extract_args_info(&input_fn.sig) { + // Extract the entry type and resource injection params + let (_, entry_type, resources) = match extract_args_info(&input_fn.sig) { Ok(info) => info, Err(e) => return e.to_compile_error().into(), }; @@ -66,13 +66,31 @@ pub fn help_attr(item: TokenStream) -> TokenStream { // Generate immutable resource bindings let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), &program_type); - // Build the render_help body with resource injection - // Use modify_res for mutable resources same pattern as renderer.rs + // Build the call to the original function with resource arguments injected + let resource_args: Vec<_> = resources + .iter() + .map(|res| { + let var_name = &res.var_name; + quote! { #var_name } + }) + .collect(); + + // Use a fixed parameter name `prev` for the trait method, regardless of + // the user's original parameter name (which may be `_` and cannot be + // referenced in expression position). + let fixed_prev: Pat = syn::parse_quote!(prev); + + let fn_call = if has_resources { + quote! { #fn_name(#fixed_prev, #(#resource_args),*) } + } else { + quote! { #fn_name(#fixed_prev) } + }; - let wrapped_body = if mut_resources.is_empty() { - quote! { #(#fn_body_stmts)* } + // Wrap the function call with modify_res for mutable resources + let inner_call = if mut_resources.is_empty() { + fn_call } else { - let mut wrapped = quote! { #(#fn_body_stmts)* }; + let mut wrapped = fn_call; for res in mut_resources.iter().rev() { let var_name = &res.var_name; let inner_type = &res.inner_type; @@ -88,10 +106,10 @@ pub fn help_attr(item: TokenStream) -> TokenStream { let help_render_body = if has_resources { quote! { #(#immut_resource_stmts)* - #wrapped_body + #inner_call } } else { - quote! { #(#fn_body_stmts)* } + quote! { #inner_call } }; // Register the help request mapping @@ -128,7 +146,7 @@ pub fn help_attr(item: TokenStream) -> TokenStream { impl ::mingling::HelpRequest for #struct_name { type Entry = #entry_type; - fn render_help(#prev_param: Self::Entry) -> ::mingling::RenderResult { + fn render_help(#fixed_prev: Self::Entry) -> ::mingling::RenderResult { let __help_result = { #help_render_body }; ::std::convert::Into::into(__help_result) } @@ -137,7 +155,6 @@ pub fn help_attr(item: TokenStream) -> TokenStream { ::mingling::macros::register_help!(#entry_type, #struct_name); // Keep the original function unchanged - #[allow(dead_code)] #(#fn_attrs)* #vis fn #fn_name(#original_inputs) -> #original_return_type { #(#fn_body_stmts)* @@ -160,7 +177,7 @@ fn build_help_entry(struct_name: &Ident, entry_type: &TypePath) -> proc_macro2:: } } -pub fn register_help(input: TokenStream) -> TokenStream { +pub(crate) fn register_help(input: TokenStream) -> TokenStream { // Parse the input as a comma-separated list of arguments let input_parsed = syn::parse_macro_input!(input with syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>::parse_terminated); diff --git a/mingling_macros/src/program_setup.rs b/mingling_macros/src/attr/program_setup.rs index 7fd9d16..dee5a1c 100644 --- a/mingling_macros/src/program_setup.rs +++ b/mingling_macros/src/attr/program_setup.rs @@ -47,7 +47,7 @@ fn extract_return_type(sig: &Signature) -> syn::Result<()> { } } -pub fn setup_attr(attr: TokenStream, item: TokenStream) -> TokenStream { +pub(crate) fn setup_attr(attr: TokenStream, item: TokenStream) -> TokenStream { // #[program_setup] takes no arguments; always use the default program path let _ = attr; let program_path = crate::default_program_path(); diff --git a/mingling_macros/src/renderer.rs b/mingling_macros/src/attr/renderer.rs index d124ec9..828dc00 100644 --- a/mingling_macros/src/renderer.rs +++ b/mingling_macros/src/attr/renderer.rs @@ -1,7 +1,7 @@ use proc_macro::TokenStream; use quote::{ToTokens, quote}; use syn::spanned::Spanned; -use syn::{ItemFn, ReturnType, Signature, TypePath, parse_macro_input}; +use syn::{ItemFn, Pat, ReturnType, Signature, TypePath, parse_macro_input}; use crate::get_global_set; use crate::res_injection::{extract_args_info, generate_immut_resource_bindings}; @@ -15,7 +15,7 @@ fn extract_user_return_type(sig: &Signature) -> Option<proc_macro2::TokenStream> } #[allow(clippy::too_many_lines)] -pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream { +pub(crate) fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream { // #[renderer] takes no arguments; always use the default program path let _ = attr; let program_path = crate::default_program_path(); @@ -31,8 +31,8 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream { .into(); } - // Extract the previous type, parameter name, and resource injection params - let (prev_param, previous_type, resources) = match extract_args_info(&input_fn.sig) { + // Extract the previous type and resource injection params + let (_, previous_type, resources) = match extract_args_info(&input_fn.sig) { Ok(info) => info, Err(e) => return e.to_compile_error().into(), }; @@ -69,31 +69,53 @@ 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(); - let inner_body_with_resources = if has_mut_resources { - let mut wrapped = quote! { #(#fn_body_stmts)* }; + // Build the call to the original function with resource arguments injected + let resource_args: Vec<_> = resources + .iter() + .map(|res| { + let var_name = &res.var_name; + quote! { #var_name } + }) + .collect(); + + // Use a fixed parameter name `prev` for the trait method, regardless of + // the user's original parameter name (which may be `_` and cannot be + // referenced in expression position). + let fixed_prev: Pat = syn::parse_quote!(prev); + + let fn_call = if has_resources { + quote! { #fn_name(#fixed_prev, #(#resource_args),*) } + } else { + quote! { #fn_name(#fixed_prev) } + }; + + // Wrap the function call with modify_res for mutable resources + let inner_call = if has_mut_resources { + let mut wrapped = fn_call; for res in mut_resources.iter().rev() { let var_name = &res.var_name; let inner_type = &res.inner_type; wrapped = quote! { - ::mingling::this::<#program_type>().modify_res(|#var_name: &mut #inner_type| { + ::mingling::this::<#program_type>() + .modify_res(|#var_name: &mut #inner_type| { #wrapped }) }; } wrapped } else { - quote! { #(#fn_body_stmts)* } + fn_call }; - // Build the Renderer::render body with resource injection - // The user's body now directly creates and returns a RenderResult. + // Build the Renderer::render body with resource injection. + // The trait method injects resources and calls the original function. let render_fn_body = if has_resources { quote! { #(#immut_resource_stmts)* - #inner_body_with_resources + #inner_call } } else { - quote! { #inner_body_with_resources } + quote! { #inner_call } }; // The original function preserves the user's exact signature and body. @@ -112,14 +134,13 @@ 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) -> ::mingling::RenderResult { + fn render(#fixed_prev: Self::Previous) -> ::mingling::RenderResult { let __renderer_result = { #render_fn_body }; ::std::convert::Into::into(__renderer_result) } } // Keep the original function unchanged - #[allow(dead_code)] #(#fn_attrs)* #vis fn #fn_name(#original_inputs) -> #original_return_type { #(#fn_body_stmts)* @@ -130,7 +151,7 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream { } /// Builds the renderer entry for the global renderers list -pub fn build_renderer_entry( +pub(crate) fn build_renderer_entry( struct_name: &syn::Ident, previous_type: &TypePath, ) -> proc_macro2::TokenStream { @@ -141,7 +162,7 @@ pub fn build_renderer_entry( } /// Builds the renderer existence check entry -pub fn build_renderer_exist_entry(previous_type: &TypePath) -> proc_macro2::TokenStream { +pub(crate) fn build_renderer_exist_entry(previous_type: &TypePath) -> proc_macro2::TokenStream { let enum_variant = &previous_type.path.segments.last().unwrap().ident; quote! { Self::#enum_variant => true, @@ -150,7 +171,9 @@ pub fn build_renderer_exist_entry(previous_type: &TypePath) -> proc_macro2::Toke /// Builds the structural renderer entry #[cfg(feature = "structural_renderer")] -pub fn build_structural_renderer_entry(previous_type: &TypePath) -> proc_macro2::TokenStream { +pub(crate) fn build_structural_renderer_entry( + previous_type: &TypePath, +) -> proc_macro2::TokenStream { let enum_variant = &previous_type.path.segments.last().unwrap().ident; quote! { Self::#enum_variant => { @@ -164,7 +187,7 @@ pub fn build_structural_renderer_entry(previous_type: &TypePath) -> proc_macro2: } } -pub fn register_renderer(input: TokenStream) -> TokenStream { +pub(crate) fn register_renderer(input: TokenStream) -> TokenStream { // Parse the input as a comma-separated list of arguments let input_parsed = syn::parse_macro_input!(input with syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>::parse_terminated); diff --git a/mingling_macros/src/derive.rs b/mingling_macros/src/derive.rs new file mode 100644 index 0000000..ffa405b --- /dev/null +++ b/mingling_macros/src/derive.rs @@ -0,0 +1,2 @@ +pub(crate) mod enum_tag; +pub(crate) mod grouped; diff --git a/mingling_macros/src/enum_tag.rs b/mingling_macros/src/derive/enum_tag.rs index 6277b69..a7f71f0 100644 --- a/mingling_macros/src/enum_tag.rs +++ b/mingling_macros/src/derive/enum_tag.rs @@ -4,7 +4,7 @@ use syn::{ Attribute, Data, DeriveInput, Error, Fields, Ident, LitStr, Result, Variant, parse_macro_input, }; -pub fn derive_enum_tag(input: TokenStream) -> TokenStream { +pub(crate) fn derive_enum_tag(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); match derive_enum_tag_impl(input) { diff --git a/mingling_macros/src/grouped.rs b/mingling_macros/src/derive/grouped.rs index 9014c37..307aab6 100644 --- a/mingling_macros/src/grouped.rs +++ b/mingling_macros/src/derive/grouped.rs @@ -2,7 +2,7 @@ use proc_macro::TokenStream; use quote::quote; use syn::{DeriveInput, Ident, parse_macro_input}; -pub fn derive_grouped(input: TokenStream) -> TokenStream { +pub(crate) fn derive_grouped(input: TokenStream) -> TokenStream { // Parse the input struct/enum let input = parse_macro_input!(input as DeriveInput); let struct_name = input.ident; diff --git a/mingling_macros/src/extensions.rs b/mingling_macros/src/extensions.rs new file mode 100644 index 0000000..022761b --- /dev/null +++ b/mingling_macros/src/extensions.rs @@ -0,0 +1,119 @@ +//! Extension point mechanism for Mingling attribute macros. +//! +//! This module provides a way for attribute macros like `#[chain]`, `#[renderer]`, +//! `#[help]`, and `#[completion]` to accept extension identifiers that are +//! applied as outer attributes before the bare macro. + +use proc_macro::TokenStream; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{Ident, Token}; + +/// Extension: `#[routeify]` — transforms `expr?` into `route!(expr)`. +#[cfg(feature = "extra_macros")] +pub(crate) mod routeify; + +/// Extension: `#[buffer]` — wraps a unit-returning function to return `RenderResult`. +pub(crate) mod buffer; + +/// Parsed extensions from an attribute macro like `#[chain(routeify, other_ext)]`. +pub(crate) struct Extensions { + pub(crate) exts: Vec<Ident>, +} + +impl Parse for Extensions { + fn parse(input: ParseStream) -> syn::Result<Self> { + let mut exts = Vec::new(); + while !input.is_empty() { + let ident: Ident = input.parse()?; + exts.push(ident); + if input.peek(Token![,]) { + let _ = input.parse::<Token![,]>(); + } + } + Ok(Extensions { exts }) + } +} + +/// Parsed extensions for `#[completion(EntryType, routeify, ...)]`. +#[cfg(feature = "comp")] +pub(crate) struct CompletionExt { + pub(crate) entry_type: proc_macro2::TokenStream, + pub(crate) exts: Vec<Ident>, +} + +#[cfg(feature = "comp")] +impl Parse for CompletionExt { + fn parse(input: ParseStream) -> syn::Result<Self> { + let entry_type: proc_macro2::TokenStream = input.parse()?; + let mut exts = Vec::new(); + while !input.is_empty() { + let _ = input.parse::<Token![,]>(); + if input.is_empty() { + break; + } + let ident: Ident = input.parse()?; + exts.push(ident); + } + Ok(CompletionExt { entry_type, exts }) + } +} + +/// Generates a re-dispatch token stream for attribute macros that take **no fixed arguments** +/// (chain, renderer, help). +pub(crate) fn try_redispatch_simple( + attr: TokenStream, + item: &TokenStream, + bare_attr_name: &str, +) -> Option<TokenStream> { + if attr.is_empty() { + return None; + } + + let exts: Extensions = syn::parse(attr).ok()?; + if exts.exts.is_empty() { + return None; + } + + let bare = Ident::new(bare_attr_name, proc_macro2::Span::call_site()); + let exts = &exts.exts; + let item = proc_macro2::TokenStream::from(item.clone()); + + Some( + quote! { + #(#[#exts])* + #[#bare] + #item + } + .into(), + ) +} + +/// Generates a re-dispatch token stream for `#[completion(EntryType, ...)]`. +#[cfg(feature = "comp")] +pub(crate) fn try_redispatch_completion( + attr: TokenStream, + item: &TokenStream, +) -> Option<TokenStream> { + if attr.is_empty() { + return None; + } + + let parsed: CompletionExt = syn::parse(attr).ok()?; + if parsed.exts.is_empty() { + return None; + } + + let entry_type = &parsed.entry_type; + let exts = &parsed.exts; + let item = proc_macro2::TokenStream::from(item.clone()); + + Some( + quote! { + #(#[#exts])* + #[::mingling::macros::completion(#entry_type)] + #item + } + .into(), + ) +} diff --git a/mingling_macros/src/extensions/buffer.rs b/mingling_macros/src/extensions/buffer.rs new file mode 100644 index 0000000..27eb612 --- /dev/null +++ b/mingling_macros/src/extensions/buffer.rs @@ -0,0 +1,95 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::spanned::Spanned; +use syn::{ItemFn, ReturnType, Type, parse_macro_input}; + +/// Checks whether the return type is unit `()`. +fn is_unit_return_type(sig: &syn::Signature) -> bool { + match &sig.output { + ReturnType::Type(_, ty) => match &**ty { + Type::Tuple(tuple) => tuple.elems.is_empty(), + _ => false, + }, + ReturnType::Default => true, + } +} + +/// The `#[buffer]` attribute macro. +/// +/// Wraps a unit-returning function to produce a `::mingling::RenderResult` by +/// injecting a local `__render_result_buffer` variable. Inside the function +/// body, the `r_print!` / `r_println!` macros can write into the buffer. +/// +/// # Example +/// +/// ```rust,ignore +/// use mingling::macros::{buffer, r_println}; +/// +/// #[buffer] +/// fn render_greeting(prev: Greeting) { +/// r_println!("Hello, {}!", *prev); +/// } +/// ``` +/// +/// Expands to: +/// +/// ```rust,ignore +/// fn render_greeting(prev: Greeting) -> mingling::RenderResult { +/// let mut __render_result_buffer = mingling::RenderResult::new(); +/// { +/// r_println!("Hello, {}!", *prev); +/// } +/// __render_result_buffer +/// } +/// ``` +pub(crate) fn buffer_impl(attr: TokenStream, item: TokenStream) -> TokenStream { + // Reject non-empty attribute arguments; #[buffer] must be bare + if !attr.is_empty() { + return syn::Error::new( + attr.into_iter().next().unwrap().span().into(), + "#[buffer] does not accept arguments", + ) + .to_compile_error() + .into(); + } + + // Parse the function item + let input_fn = parse_macro_input!(item as ItemFn); + + // Validate the function is not async + if input_fn.sig.asyncness.is_some() { + return syn::Error::new(input_fn.sig.span(), "Buffer function cannot be async") + .to_compile_error() + .into(); + } + + // Validate return type is unit + if !is_unit_return_type(&input_fn.sig) { + return syn::Error::new( + input_fn.sig.span(), + "#[buffer] function must not have a return value (must return `()`)", + ) + .to_compile_error() + .into(); + } + + // Get function attributes (excluding the buffer attribute) + let mut fn_attrs = input_fn.attrs.clone(); + fn_attrs.retain(|attr| !attr.path().is_ident("buffer")); + + let vis = &input_fn.vis; + let fn_name = &input_fn.sig.ident; + let inputs = &input_fn.sig.inputs; + let fn_body = &input_fn.block; + + let expanded = quote! { + #(#fn_attrs)* + #vis fn #fn_name(#inputs) -> ::mingling::RenderResult { + let mut __render_result_buffer = ::mingling::RenderResult::new(); + #fn_body + __render_result_buffer + } + }; + + expanded.into() +} diff --git a/mingling_macros/src/extensions/routeify.rs b/mingling_macros/src/extensions/routeify.rs new file mode 100644 index 0000000..7cded54 --- /dev/null +++ b/mingling_macros/src/extensions/routeify.rs @@ -0,0 +1,46 @@ +//! The `#[routeify]` extension — transforms `expr?` into `route!(expr)`. +//! +//! Designed as an extension for the Mingling attribute macro system, intended +//! to be used with `#[chain(routeify)]` or standalone as `#[routeify]`. +//! +//! # How it works +//! +//! The macro parses the function AST and replaces every `Expr::Try` node with an +//! equivalent `route!(expr)` invocation. + +use proc_macro::TokenStream; +use quote::ToTokens; +use syn::spanned::Spanned; +use syn::visit_mut::VisitMut; +use syn::{Expr, ItemFn, parse_macro_input}; + +struct RouteifyTransform; + +impl VisitMut for RouteifyTransform { + fn visit_expr_mut(&mut self, expr: &mut Expr) { + syn::visit_mut::visit_expr_mut(self, expr); + + if let Expr::Try(try_expr) = expr { + let inner = &*try_expr.expr; + let inner_tokens = inner.to_token_stream(); + + // Set the span of the generated `route` ident to the `?` token's span, + // so that rust-analyzer resolves the `?` position to the `route!` macro + // instead of the standard Try trait, showing the route macro's docs on hover. + let q_span = try_expr.question_token.span(); + let route_ident = proc_macro2::Ident::new("route", q_span); + + if let Ok(macro_expr) = syn::parse2::<Expr>(quote::quote! { + ::mingling::macros::#route_ident!(#inner_tokens) + }) { + *expr = macro_expr; + } + } + } +} + +pub(crate) fn routeify_impl(_attr: TokenStream, item: TokenStream) -> TokenStream { + let mut input_fn = parse_macro_input!(item as ItemFn); + RouteifyTransform.visit_item_fn_mut(&mut input_fn); + input_fn.to_token_stream().into() +} diff --git a/mingling_macros/src/func.rs b/mingling_macros/src/func.rs new file mode 100644 index 0000000..33bb094 --- /dev/null +++ b/mingling_macros/src/func.rs @@ -0,0 +1,13 @@ +pub(crate) mod dispatcher; +#[cfg(feature = "extra_macros")] +pub(crate) mod entry; +pub(crate) mod gen_program; +#[cfg(feature = "extra_macros")] +pub(crate) mod group; +pub(crate) mod node; +pub(crate) mod pack; +#[cfg(feature = "extra_macros")] +pub(crate) mod pack_err; +pub(crate) mod r_print; +#[cfg(feature = "comp")] +pub(crate) mod suggest; diff --git a/mingling_macros/src/dispatcher.rs b/mingling_macros/src/func/dispatcher.rs index 3698ede..a61dd26 100644 --- a/mingling_macros/src/dispatcher.rs +++ b/mingling_macros/src/func/dispatcher.rs @@ -75,7 +75,7 @@ impl Parse for DispatcherChainInput { // are nearly identical and could benefit from refactoring into common helper functions. #[allow(clippy::too_many_lines)] -pub fn dispatcher(input: TokenStream) -> TokenStream { +pub(crate) fn dispatcher(input: TokenStream) -> TokenStream { // Parse the input let dispatcher_input = syn::parse_macro_input!(input as DispatcherChainInput); @@ -135,7 +135,7 @@ pub fn dispatcher(input: TokenStream) -> TokenStream { } fn begin(&self, args: Vec<String>) -> ::mingling::ChainProcess<#program_type> { use ::mingling::Grouped; - #pack::new(args).to_chain() + ::mingling::Routable::to_chain(#pack::new(args)) } fn clone_dispatcher(&self) -> Box<dyn ::mingling::Dispatcher<#program_type>> { Box::new(#command_struct) @@ -209,7 +209,7 @@ impl Parse for RegisterDispatcherInput { } #[cfg(feature = "dispatch_tree")] -pub fn register_dispatcher(input: TokenStream) -> TokenStream { +pub(crate) fn register_dispatcher(input: TokenStream) -> TokenStream { let RegisterDispatcherInput { node_name, dispatcher_type, @@ -243,7 +243,7 @@ pub fn register_dispatcher(input: TokenStream) -> TokenStream { } #[cfg(not(feature = "dispatch_tree"))] -pub fn register_dispatcher(_input: TokenStream) -> TokenStream { +pub(crate) fn register_dispatcher(_input: TokenStream) -> TokenStream { quote! {}.into() } diff --git a/mingling_macros/src/entry.rs b/mingling_macros/src/func/entry.rs index 2ac5d6b..35209e5 100644 --- a/mingling_macros/src/entry.rs +++ b/mingling_macros/src/func/entry.rs @@ -39,7 +39,7 @@ fn parse_strings(input: &syn::parse::ParseBuffer) -> syn::Result<Vec<String>> { Ok(strings) } -pub fn entry(input: TokenStream) -> TokenStream { +pub(crate) fn entry(input: TokenStream) -> TokenStream { let parsed = parse_macro_input!(input as EntryInput); let strings = match &parsed { diff --git a/mingling_macros/src/func/gen_program.rs b/mingling_macros/src/func/gen_program.rs new file mode 100644 index 0000000..de4da06 --- /dev/null +++ b/mingling_macros/src/func/gen_program.rs @@ -0,0 +1,628 @@ +use std::collections::HashMap; + +use proc_macro::TokenStream; +use quote::quote; +use syn::parse_macro_input; + +use crate::CHAINS; +use crate::CHAINS_EXIST; +#[cfg(feature = "dispatch_tree")] +use crate::COMPILE_TIME_DISPATCHERS; +#[cfg(feature = "comp")] +use crate::COMPLETIONS; +use crate::HELP_REQUESTS; +use crate::PACKED_TYPES; +use crate::RENDERERS; +use crate::RENDERERS_EXIST; +#[cfg(feature = "structural_renderer")] +use crate::STRUCTURAL_RENDERERS; +use crate::attr::{chain, renderer}; +use crate::get_global_set; +#[cfg(feature = "dispatch_tree")] +use crate::systems::dispatch_tree_gen; + +#[cfg(feature = "async")] +const ASYNC_ENABLED: bool = true; +#[cfg(not(feature = "async"))] +const ASYNC_ENABLED: bool = false; + +/// Parses an entry of the format `StructName => EnumVariant,` into a pair of idents. +fn parse_entry_pair(entry: &proc_macro2::TokenStream) -> (proc_macro2::Ident, proc_macro2::Ident) { + let s = entry.to_string(); + let arrow_idx = s + .find("=>") + .unwrap_or_else(|| panic!("Entry missing '=>': {s}")); + let struct_str = s[..arrow_idx].trim(); + let variant_str = s[arrow_idx + 2..].trim().trim_end_matches(',').trim(); + let struct_ident = proc_macro2::Ident::new(struct_str, proc_macro2::Span::call_site()); + let variant_ident = proc_macro2::Ident::new(variant_str, proc_macro2::Span::call_site()); + (struct_ident, variant_ident) +} + +/// Loads the pathf type mapping from `$OUT_DIR/{crate}/type_using.rs`. +/// Always compiled; returns None when pathf feature is not enabled, +/// or when the file does not exist / fails to load. +fn load_pathf_map() -> Option<std::collections::HashMap<String, String>> { + if !cfg!(feature = "pathf") { + return None; + } + let out_dir = std::env::var("OUT_DIR").ok()?; + let crate_name = std::env::var("CARGO_PKG_NAME").ok()?; + let path = std::path::Path::new(&out_dir) + .join(&crate_name) + .join("type_using.rs"); + let content = std::fs::read_to_string(&path).ok()?; + Some( + content + .lines() + .filter_map(|line| { + let line = line.trim(); + if let Some(rest) = line.strip_prefix("use ") { + let path = rest.strip_suffix(';').unwrap_or(rest); + if let Some((_mod, type_name)) = path.rsplit_once("::") { + return Some((type_name.to_string(), path.to_string())); + } + } + None + }) + .collect(), + ) +} + +/// Resolves a type name to its full path token stream using the pathf mapping. +pub(crate) fn resolve_type( + name: &str, + map: &std::collections::HashMap<String, String>, +) -> proc_macro2::TokenStream { + if let Some(full_path) = map.get(name) { + syn::parse_str::<proc_macro2::TokenStream>(full_path).unwrap_or_else(|_| { + let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site()); + quote! { #ident } + }) + } else { + let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site()); + quote! { #ident } + } +} + +pub(crate) fn gen_program_impl(_input: TokenStream) -> TokenStream { + #[cfg(feature = "comp")] + let comp_gen = quote! { + ::mingling::macros::program_comp_gen!(); + }; + + #[cfg(not(feature = "comp"))] + let comp_gen = quote! {}; + + TokenStream::from(quote! { + /// Alias for the current program type `crate::ThisProgram` + pub type Next = ::mingling::ChainProcess<crate::ThisProgram>; + + impl ::mingling::Routable<crate::ThisProgram> for ::mingling::ChainProcess<crate::ThisProgram> + { + fn to_chain(self) -> ::mingling::ChainProcess<crate::ThisProgram> { + match self { + ::mingling::ChainProcess::Ok((any, _)) => { + ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Chain)) + } + other => other, + } + } + + fn to_render(self) -> ::mingling::ChainProcess<crate::ThisProgram> { + match self { + ::mingling::ChainProcess::Ok((any, _)) => { + ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Renderer)) + } + other => other, + } + } + } + + #comp_gen + ::mingling::macros::program_fallback_gen!(); + ::mingling::macros::program_final_gen!(); + }) +} + +#[cfg(feature = "comp")] +pub(crate) fn program_comp_gen_impl(_input: TokenStream) -> TokenStream { + #[cfg(feature = "async")] + let fn_exec_comp = quote! { + #[doc(hidden)] + #[::mingling::macros::chain] + pub async fn __exec_completion(prev: CompletionContext) -> Next { + use ::mingling::Grouped; + + let read_ctx = ::mingling::ShellContext::try_from(prev.inner); + match read_ctx { + Ok(ctx) => { + let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx); + ::mingling::Routable::<crate::ThisProgram>::to_render(crate::CompletionSuggest::new((ctx, suggest))) + } + Err(_) => std::process::exit(1), + } + } + }; + + #[cfg(not(feature = "async"))] + let fn_exec_comp = quote! { + #[doc(hidden)] + #[::mingling::macros::chain] + pub fn __exec_completion(prev: CompletionContext) -> Next { + use ::mingling::Grouped; + + let read_ctx = ::mingling::ShellContext::try_from(prev.inner); + match read_ctx { + Ok(ctx) => { + let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx); + ::mingling::Routable::<crate::ThisProgram>::to_render(crate::CompletionSuggest::new((ctx, suggest))) + } + Err(_) => std::process::exit(1), + } + } + }; + + #[cfg(feature = "dispatch_tree")] + let internal_dispatcher_comp = quote! { + use __internal_completion_mod::__internal_dispatcher_comp; + }; + + #[cfg(not(feature = "dispatch_tree"))] + let internal_dispatcher_comp = quote! {}; + + let comp_dispatcher = quote! { + #[doc(hidden)] + mod __internal_completion_mod { + use ::mingling::Grouped; + ::mingling::macros::dispatcher!("__comp", CMDCompletion => CompletionContext); + ::mingling::macros::pack!( + CompletionSuggest = (::mingling::ShellContext, ::mingling::Suggest) + ); + } + #internal_dispatcher_comp + use __internal_completion_mod::CompletionContext; + use __internal_completion_mod::CompletionSuggest; + pub use __internal_completion_mod::CMDCompletion; + + #fn_exec_comp + + ::mingling::macros::register_type!(CompletionContext); + + #[allow(unused)] + #[doc(hidden)] + #[::mingling::macros::renderer] + 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 + } + }; + + TokenStream::from(comp_dispatcher) +} + +pub(crate) fn register_type_impl(input: TokenStream) -> TokenStream { + let type_ident = parse_macro_input!(input as syn::Ident); + let entry_str = type_ident.to_string(); + + get_global_set(&PACKED_TYPES) + .lock() + .unwrap() + .insert(entry_str); + + TokenStream::new() +} + +pub(crate) fn register_chain_impl(input: TokenStream) -> TokenStream { + chain::register_chain(input) +} + +pub(crate) fn register_renderer_impl(input: TokenStream) -> TokenStream { + renderer::register_renderer(input) +} + +pub(crate) fn program_fallback_gen_impl(_input: TokenStream) -> TokenStream { + #[cfg(feature = "structural_renderer")] + let pack_empty = quote! { + #[derive(::serde::Serialize, ::mingling::StructuralData, ::mingling::Grouped, Default)] + pub struct ResultEmpty; + }; + + #[cfg(not(feature = "structural_renderer"))] + let pack_empty = quote! { + #[derive(::mingling::Grouped, Default)] + pub struct ResultEmpty; + }; + + let expanded = quote! { + ::mingling::macros::pack!(ErrorRendererNotFound = String); + ::mingling::macros::pack!(ErrorDispatcherNotFound = Vec<String>); + #pack_empty + }; + TokenStream::from(expanded) +} + +#[allow(clippy::too_many_lines)] +pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { + let name = syn::Ident::new("ThisProgram", proc_macro2::Span::call_site()); + + let packed_types = get_global_set(&PACKED_TYPES).lock().unwrap().clone(); + + let renderers = get_global_set(&RENDERERS).lock().unwrap().clone(); + let chains = get_global_set(&CHAINS).lock().unwrap().clone(); + let renderer_exist = get_global_set(&RENDERERS_EXIST).lock().unwrap().clone(); + let chain_exist = get_global_set(&CHAINS_EXIST).lock().unwrap().clone(); + + #[cfg(feature = "structural_renderer")] + let structural_renderers = get_global_set(&STRUCTURAL_RENDERERS) + .lock() + .unwrap() + .clone(); + + #[cfg(feature = "comp")] + let completions = get_global_set(&COMPLETIONS).lock().unwrap().clone(); + + let packed_types: Vec<proc_macro2::TokenStream> = packed_types + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + let renderer_tokens: Vec<proc_macro2::TokenStream> = renderers + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + let chain_tokens: Vec<proc_macro2::TokenStream> = chains + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + let renderer_exist_tokens: Vec<proc_macro2::TokenStream> = renderer_exist + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + let chain_exist_tokens: Vec<proc_macro2::TokenStream> = chain_exist + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + let pathf_map: Option<HashMap<String, String>> = load_pathf_map(); + + #[cfg(feature = "pathf")] + let pathf_hint: proc_macro2::TokenStream = if pathf_map.is_none() { + quote! { + compile_error!( +"Cannot load type mapping computed by `pathf`. +If not yet configured, execute `mingling::build::analyze_and_build_type_mapping()` in your `build.rs`. + +fn main() { + // Require features: [\"builds\", \"pathf\"] + mingling::build::analyze_and_build_type_mapping().unwrap(); + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\__ Write to `build.rs` + +} + "); + } + } else { + quote! {} + }; + + #[cfg(not(feature = "pathf"))] + let pathf_hint: proc_macro2::TokenStream = quote! {}; + + let pathf_map: HashMap<String, String> = if cfg!(feature = "pathf") { + pathf_map.unwrap_or_default() + } else { + HashMap::new() + }; + + let pathf_uses: Vec<proc_macro2::TokenStream> = if cfg!(feature = "pathf") { + pathf_map + .values() + .map(|path| format!("use {};", path).parse().unwrap_or_default()) + .collect() + } else { + Vec::new() + }; + + #[cfg(feature = "structural_renderer")] + let structural_renderer_tokens: Vec<proc_macro2::TokenStream> = structural_renderers + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + #[cfg(feature = "structural_renderer")] + let structural_render = quote! { + fn structural_render( + any: ::mingling::AnyOutput<Self::Enum>, + setting: &::mingling::StructuralRendererSetting, + ) -> Result<::mingling::RenderResult, ::mingling::error::StructuralRendererSerializeError> { + #[allow(unused_imports)] + #(#pathf_uses)* + match any.member_id { + #(#structural_renderer_tokens)* + _ => { + // Non-structural types: render ResultEmpty (which implements + // StructuralData + Serialize) instead of producing nothing. + let mut r = ::mingling::RenderResult::default(); + ::mingling::StructuralRenderer::render(&ResultEmpty, setting, &mut r)?; + Ok(r) + } + } + } + }; + + #[cfg(not(feature = "structural_renderer"))] + let structural_render = quote! {}; + + #[cfg(feature = "dispatch_tree")] + let compile_time_dispatchers: Vec<String> = get_global_set(&COMPILE_TIME_DISPATCHERS) + .lock() + .unwrap() + .clone() + .iter() + .cloned() + .collect(); + + #[cfg(feature = "dispatch_tree")] + let dispatch_tree_nodes = { + let entries: Vec<(String, String, String)> = compile_time_dispatchers + .iter() + .filter_map(|entry| { + let parts: Vec<&str> = entry.split(':').collect(); + if parts.len() == 3 { + Some(( + parts[0].to_string(), + parts[1].to_string(), + parts[2].to_string(), + )) + } else { + None + } + }) + .collect(); + + let get_nodes_fn = dispatch_tree_gen::gen_get_nodes(&entries, &pathf_map); + let dispatch_trie_fn = dispatch_tree_gen::gen_dispatch_args_trie(&entries, &pathf_map); + + quote! { + #get_nodes_fn + #dispatch_trie_fn + } + }; + + #[cfg(not(feature = "dispatch_tree"))] + let dispatch_tree_nodes = quote! {}; + + #[cfg(feature = "comp")] + let completion_tokens: Vec<proc_macro2::TokenStream> = completions + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + #[cfg(feature = "comp")] + let comp = quote! { + fn do_comp(any: &::mingling::AnyOutput<Self::Enum>, ctx: &::mingling::ShellContext) -> ::mingling::Suggest { + #[allow(unused_imports)] + #(#pathf_uses)* + match any.member_id { + #(#completion_tokens)* + _ => ::mingling::Suggest::FileCompletion, + } + } + }; + + #[cfg(not(feature = "comp"))] + 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>) -> ::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); + quote! { + Self::#variant_ident => { + // 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) + } + } + }).collect(); + 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| { + 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); + quote! { + Self::#variant_ident => { + // 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() }; + let fut = async { <#resolved_struct as ::mingling::Chain<Self::Enum>>::proc(value).await }; + ::std::boxed::Box::pin(fut) + } + } + }).collect(); + + let chain_arms_sync: Vec<_> = chain_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); + quote! { + Self::#variant_ident => { + // 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::Chain<Self::Enum>>::proc(value) + } + } + }) + .collect(); + + let do_chain_fn = if chain_tokens.is_empty() { + quote! { + fn do_chain(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::ChainProcess<Self::Enum> { + ::core::panic!("No chain found for type id") + } + } + } else if ASYNC_ENABLED { + quote! { + fn do_chain( + any: ::mingling::AnyOutput<Self::Enum>, + ) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::mingling::ChainProcess<Self::Enum>> + ::std::marker::Send>> { + match any.member_id { + #(#chain_arms_async)* + _ => ::core::panic!("No chain found for type id: {:?}", any.type_id), + } + } + } + } else { + quote! { + fn do_chain( + any: ::mingling::AnyOutput<Self::Enum>, + ) -> ::mingling::ChainProcess<Self::Enum> { + match any.member_id { + #(#chain_arms_sync)* + _ => ::core::panic!("No chain found for type id: {:?}", any.type_id), + } + } + } + }; + + let help_tokens: Vec<proc_macro2::TokenStream> = get_global_set(&HELP_REQUESTS) + .lock() + .unwrap() + .clone() + .iter() + .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) + .collect(); + + let num_variants = packed_types.len(); + let repr_type = if u8::try_from(num_variants).is_ok() { + quote! { u8 } + } else if u16::try_from(num_variants).is_ok() { + quote! { u16 } + } else if u32::try_from(num_variants).is_ok() { + quote! { u32 } + } else { + quote! { u128 } + }; + + let expanded = quote! { + #pathf_hint + + #[derive(Debug, PartialEq, Eq, Clone)] + #[repr(#repr_type)] + #[allow(nonstandard_style)] + pub enum #name { + #(#packed_types),* + } + + impl ::std::fmt::Display for #name { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match self { + #(#name::#packed_types => write!(f, stringify!(#packed_types)),)* + } + } + } + + impl ::mingling::ProgramCollect for #name { + type Enum = #name; + type ErrorDispatcherNotFound = ErrorDispatcherNotFound; + type ErrorRendererNotFound = ErrorRendererNotFound; + type ResultEmpty = ResultEmpty; + fn build_renderer_not_found(member_id: Self::Enum) -> ::mingling::AnyOutput<Self::Enum> { + ::mingling::AnyOutput::new(ErrorRendererNotFound::new(member_id.to_string())) + } + fn build_dispatcher_not_found(args: Vec<String>) -> ::mingling::AnyOutput<Self::Enum> { + ::mingling::AnyOutput::new(ErrorDispatcherNotFound::new(args)) + } + fn build_empty_result() -> ::mingling::AnyOutput<Self::Enum> { + ::mingling::AnyOutput::new(ResultEmpty) + } + #render_fn + #do_chain_fn + 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 { + match any.member_id { + #(#renderer_exist_tokens)* + _ => false + } + } + fn has_chain(any: &::mingling::AnyOutput<Self::Enum>) -> bool { + match any.member_id { + #(#chain_exist_tokens)* + _ => false + } + } + #dispatch_tree_nodes + #structural_render + #comp + } + + impl #name { + /// Creates a new `Program<#name>` instance with default configuration. + pub fn new() -> ::mingling::Program<#name> { + ::mingling::Program::new() + } + + /// Returns a static reference to the global `Program<#name>` singleton. + pub fn this() -> &'static ::mingling::Program<#name> { + &::mingling::this::<#name>() + } + } + }; + + // Clear all global registries to prevent stale state in Rust Analyzer + get_global_set(&PACKED_TYPES).lock().unwrap().clear(); + get_global_set(&CHAINS).lock().unwrap().clear(); + get_global_set(&CHAINS_EXIST).lock().unwrap().clear(); + get_global_set(&RENDERERS).lock().unwrap().clear(); + get_global_set(&RENDERERS_EXIST).lock().unwrap().clear(); + get_global_set(&HELP_REQUESTS).lock().unwrap().clear(); + #[cfg(feature = "comp")] + get_global_set(&COMPLETIONS).lock().unwrap().clear(); + #[cfg(feature = "dispatch_tree")] + get_global_set(&COMPILE_TIME_DISPATCHERS) + .lock() + .unwrap() + .clear(); + #[cfg(feature = "structural_renderer")] + get_global_set(&STRUCTURAL_RENDERERS) + .lock() + .unwrap() + .clear(); + + TokenStream::from(expanded) +} diff --git a/mingling_macros/src/group_impl.rs b/mingling_macros/src/func/group.rs index cac2734..b865913 100644 --- a/mingling_macros/src/group_impl.rs +++ b/mingling_macros/src/func/group.rs @@ -91,7 +91,7 @@ fn gen_type_use(type_path: &TypePath) -> proc_macro2::TokenStream { } } -pub fn group_macro(input: TokenStream) -> TokenStream { +pub(crate) fn group_macro(input: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(input as GroupInput); let is_aliased = matches!(input, GroupInput::Aliased { .. }); diff --git a/mingling_macros/src/node.rs b/mingling_macros/src/func/node.rs index b3a61c6..1b944a1 100644 --- a/mingling_macros/src/node.rs +++ b/mingling_macros/src/func/node.rs @@ -18,7 +18,7 @@ impl Parse for NodeInput { } } -pub fn node(input: TokenStream) -> TokenStream { +pub(crate) fn node(input: TokenStream) -> TokenStream { // Parse the input as a string literal let input_parsed = syn::parse_macro_input!(input as NodeInput); let path_str = input_parsed.path.value(); diff --git a/mingling_macros/src/pack.rs b/mingling_macros/src/func/pack.rs index 0af1b4c..a1a7e6b 100644 --- a/mingling_macros/src/pack.rs +++ b/mingling_macros/src/func/pack.rs @@ -25,7 +25,7 @@ impl Parse for PackInput { } #[allow(clippy::too_many_lines)] -pub fn pack(input: TokenStream) -> TokenStream { +pub(crate) fn pack(input: TokenStream) -> TokenStream { let pack_input = syn::parse_macro_input!(input as PackInput); let group_name = crate::default_program_path(); diff --git a/mingling_macros/src/pack_err.rs b/mingling_macros/src/func/pack_err.rs index a747aa9..36e550a 100644 --- a/mingling_macros/src/pack_err.rs +++ b/mingling_macros/src/func/pack_err.rs @@ -31,7 +31,7 @@ impl syn::parse::Parse for PackErrInput { } #[allow(clippy::too_many_lines)] -pub fn pack_err(input: TokenStream) -> TokenStream { +pub(crate) fn pack_err(input: TokenStream) -> TokenStream { let parsed = parse_macro_input!(input as PackErrInput); match parsed { @@ -123,7 +123,7 @@ pub fn pack_err(input: TokenStream) -> TokenStream { /// impl ::mingling::__private::StructuralData for ErrorNotFound {} /// ``` #[cfg(feature = "structural_renderer")] -pub fn pack_err_structural(input: TokenStream) -> TokenStream { +pub(crate) fn pack_err_structural(input: TokenStream) -> TokenStream { let parsed = parse_macro_input!(input as PackErrInput); let type_name = match &parsed { diff --git a/mingling_macros/src/func/r_print.rs b/mingling_macros/src/func/r_print.rs new file mode 100644 index 0000000..e81b544 --- /dev/null +++ b/mingling_macros/src/func/r_print.rs @@ -0,0 +1,62 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{Ident, Token}; + +/// Parsed input for `r_println!` and `r_print!`. +/// +/// Two forms: +/// - `(ident, format_args...)` — explicit buffer +/// - `(format_args...)` — implicit `__render_result_buffer` +enum PrintInput { + Explicit { dst: Ident, args: TokenStream2 }, + Implicit { args: TokenStream2 }, +} + +impl Parse for PrintInput { + fn parse(input: ParseStream) -> syn::Result<Self> { + // Peek: if the next token is an ident followed by a comma, it's the explicit form + if input.peek(Ident) && input.peek2(Token![,]) { + let dst: Ident = input.parse()?; + let _comma: Token![,] = input.parse()?; + let args: TokenStream2 = input.parse()?; + Ok(PrintInput::Explicit { dst, args }) + } else { + let args: TokenStream2 = input.parse()?; + Ok(PrintInput::Implicit { args }) + } + } +} + +fn expand_print(input: TokenStream, method: &str) -> TokenStream { + let parsed: PrintInput = match syn::parse(input) { + Ok(p) => p, + Err(e) => return e.to_compile_error().into(), + }; + + let method_ident = Ident::new(method, proc_macro2::Span::call_site()); + + let expanded = match parsed { + PrintInput::Explicit { dst, args } => { + quote! { + #dst.#method_ident(format!(#args)) + } + } + PrintInput::Implicit { args } => { + quote! { + __render_result_buffer.#method_ident(format!(#args)) + } + } + }; + + expanded.into() +} + +pub(crate) fn r_println(input: TokenStream) -> TokenStream { + expand_print(input, "println") +} + +pub(crate) fn r_print(input: TokenStream) -> TokenStream { + expand_print(input, "print") +} diff --git a/mingling_macros/src/suggest.rs b/mingling_macros/src/func/suggest.rs index d3ab446..0f2026f 100644 --- a/mingling_macros/src/suggest.rs +++ b/mingling_macros/src/func/suggest.rs @@ -35,7 +35,7 @@ impl Parse for SuggestItem { } #[cfg(feature = "comp")] -pub fn suggest(input: TokenStream) -> TokenStream { +pub(crate) fn suggest(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as SuggestInput); let mut items = Vec::new(); @@ -71,7 +71,7 @@ pub fn suggest(input: TokenStream) -> TokenStream { expanded.into() } -pub fn suggest_enum(input: TokenStream) -> TokenStream { +pub(crate) fn suggest_enum(input: TokenStream) -> TokenStream { let enum_type = parse_macro_input!(input as syn::Type); let expanded = quote! {{ diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index ea33577..56ed999 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -141,40 +141,50 @@ //! } //! ``` -use proc_macro::TokenStream; +#![deny(missing_docs)] + +#[cfg(feature = "extra_macros")] use quote::quote; + +#[cfg(feature = "extra_macros")] +use syn::parse_macro_input; + +use proc_macro::TokenStream; use std::collections::BTreeSet; use std::sync::Mutex; use std::sync::OnceLock; -use syn::parse_macro_input; -mod chain; +mod attr; +mod derive; +mod func; +mod systems; + +mod extensions; +mod utils; + +// Bring all sub-modules into scope at the old paths so that existing +// references (e.g. `chain::chain_attr`, `renderer::renderer_attr`) +// continue to work without any `use`-path changes. #[cfg(feature = "comp")] -mod completion; -#[cfg(feature = "dispatch_tree")] -mod dispatch_tree_gen; -mod dispatcher; +use attr::completion; #[cfg(feature = "clap")] -mod dispatcher_clap; +use attr::dispatcher_clap; #[cfg(feature = "extra_macros")] -mod entry; -mod enum_tag; +use attr::program_setup; +use attr::{chain, help, renderer}; +use derive::{enum_tag, grouped}; #[cfg(feature = "extra_macros")] -mod group_impl; -mod grouped; -mod help; -mod node; -mod pack; +use func::entry; #[cfg(feature = "extra_macros")] -mod pack_err; +pub(crate) use func::group as group_impl; #[cfg(feature = "extra_macros")] -mod program_setup; -mod renderer; -mod res_injection; -#[cfg(feature = "structural_renderer")] -mod structural_data; +use func::pack_err; #[cfg(feature = "comp")] -mod suggest; +use func::suggest; +use func::{dispatcher, node, pack}; +use systems::res_injection; +#[cfg(feature = "structural_renderer")] +pub(crate) use systems::structural_data; pub(crate) fn default_program_path() -> proc_macro2::TokenStream { quote::quote! { crate::ThisProgram } @@ -262,44 +272,36 @@ fn entry_has_variant(entry: &str, variant_name: &str) -> bool { entry.contains(&format!(":: {variant_name} =>")) } -/// Registers an outside-type as a member of a program group without modifying its definition. +/// Registers an outside type (from `std` or other crates) as a type recognizable +/// by the Mingling framework, without modifying the original type definition. /// -/// This macro allows you to use outside-types from external crates (like `std::io::Error`) -/// within the Mingling framework by generating a `Grouped` implementation and registering -/// the type's simple name as an enum variant. +/// This macro generates a newtype wrapper around the given type that implements +/// `Grouped`, `Into<AnyOutput>`, `Into<ChainProcess>`, and the `Routable` trait, +/// making the outside type usable in `#[chain]` and `#[renderer]` functions. /// /// # Syntax /// /// ```rust,ignore -/// group!(std::io::Error); +/// // Simple form — creates a wrapper named after the type's last segment: /// group!(ParseIntError); -/// ``` /// -/// The type is registered under the default program (`crate::ThisProgram`). -/// -/// # How it works -/// -/// The macro generates a module containing: -/// - A `use` import for the program path and the outside-type -/// - An `impl Grouped<Program>` for the outside-type -/// - A `register_type!` call with the type's simple name -/// -/// The type's simple name (e.g. `Error`) is used as the enum variant in the generated -/// program enum, just like `#[derive(Grouped)]` or `pack!`. +/// // Aliased form — creates a wrapper with a custom name: +/// group!(ErrorIo = std::io::Error); +/// ``` /// /// # Example /// -/// ```rust,ignore -/// use mingling::macros::group; -/// -/// // Register std::io::Error as a group member -/// group!(std::io::Error); +/// See the full example in the crate documentation or run: +/// ```bash +/// cargo run --example example-outside-type -- parse 42 +/// cargo run --example example-outside-type -- parse hello +/// cargo run --example example-outside-type -- error /// ``` /// -/// After expansion, the type can be used in chains and renderers like any -/// `#[derive(Grouped)]` type. +/// # Requirements /// -/// This macro is only available with the `extra_macros` feature. +/// - The type must be accessible at the call site (imported or fully qualified). +/// - The alias name (if provided) must not conflict with existing types. #[cfg(feature = "extra_macros")] #[proc_macro] pub fn group(input: TokenStream) -> TokenStream { @@ -541,6 +543,28 @@ pub fn pack_err_structural(input: TokenStream) -> TokenStream { /// directly, while the `Err` value is converted via `Routable::to_chain()` and /// returned early. /// +/// ## Interaction with `#[routeify]` +/// +/// The [`#[routeify]`](attr.routeify.html) attribute macro automatically replaces +/// every `expr?` inside a function with `route!(expr)`. This means you can use the +/// familiar `?` syntax in chain functions instead of writing `route!(...)` +/// explicitly: +/// +/// ```rust,ignore +/// use mingling::macros::chain; +/// +/// #[chain(routeify)] +/// fn process(prev: SomeEntry) -> Next { +/// // `?` here expands to `route!(...)` → this macro → the match block +/// let value = some_fallible_call()?; +/// value.to_chain() +/// } +/// ``` +/// +/// Because `#[routeify]` maps the span of `?` to this macro, hovering over `?` in +/// a `#[routeify]` function will display this documentation — explaining what +/// the `?` actually expands to. +/// /// # Example /// /// ```rust,ignore @@ -618,7 +642,7 @@ pub fn route(input: TokenStream) -> TokenStream { #[proc_macro] pub fn empty_result(_input: TokenStream) -> TokenStream { let expanded = quote! { - <crate::ResultEmpty as ::mingling::Grouped::<crate::ThisProgram>>::to_chain(crate::ResultEmpty) + <crate::ResultEmpty as ::mingling::Routable::<crate::ThisProgram>>::to_chain(crate::ResultEmpty) }; TokenStream::from(expanded) } @@ -877,6 +901,11 @@ pub fn dispatcher(input: TokenStream) -> TokenStream { /// - With the `async` feature, async functions are supported; without it, async functions are rejected. #[proc_macro_attribute] pub fn chain(attr: TokenStream, item: TokenStream) -> TokenStream { + // Extension point: if attr contains extension identifiers like `routeify`, + // re-dispatch as #[ext1] #[ext2] #[chain] fn ... + if let Some(redispatch) = extensions::try_redispatch_simple(attr.clone(), &item, "chain") { + return redispatch; + } chain::chain_attr(attr, item) } @@ -944,6 +973,9 @@ pub fn chain(attr: TokenStream, item: TokenStream) -> TokenStream { /// ``` #[proc_macro_attribute] pub fn renderer(attr: TokenStream, item: TokenStream) -> TokenStream { + if let Some(redispatch) = extensions::try_redispatch_simple(attr.clone(), &item, "renderer") { + return redispatch; + } renderer::renderer_attr(attr, item) } @@ -998,6 +1030,9 @@ pub fn renderer(attr: TokenStream, item: TokenStream) -> TokenStream { #[cfg(feature = "comp")] #[proc_macro_attribute] pub fn completion(attr: TokenStream, item: TokenStream) -> TokenStream { + if let Some(redispatch) = extensions::try_redispatch_completion(attr.clone(), &item) { + return redispatch; + } completion::completion_attr(attr, item) } @@ -1251,10 +1286,132 @@ pub fn register_dispatcher(input: TokenStream) -> TokenStream { /// /// [`BasicProgramSetup`]: https://docs.rs/mingling/latest/mingling/setup/struct.BasicProgramSetup.html #[proc_macro_attribute] -pub fn help(_attr: TokenStream, item: TokenStream) -> TokenStream { +pub fn help(attr: TokenStream, item: TokenStream) -> TokenStream { + if let Some(redispatch) = extensions::try_redispatch_simple(attr.clone(), &item, "help") { + return redispatch; + } help::help_attr(item) } +/// Extension attribute macro that transforms `expr?` into `route!(expr)`. +/// +/// Designed for use with `#[chain(routeify, ...)]` to enable concise error +/// routing in chain functions using the `?` operator syntax. +/// +/// # Example +/// +/// ```rust,ignore +/// #[chain(routeify)] +/// fn handle_calc(args: EntryCalculate) -> Next { +/// let a = args.pick(&arg![f32]).to_result()?; +/// let op = args.pick(&arg![Operator]).to_result()?; +/// StateCalculate { number_a: a, operator: op, ... }.to_chain() +/// } +/// ``` +#[cfg(feature = "extra_macros")] +#[proc_macro_attribute] +pub fn routeify(attr: TokenStream, item: TokenStream) -> TokenStream { + extensions::routeify::routeify_impl(attr, item) +} + +/// Wraps a unit-returning function to produce a `RenderResult`. +/// +/// The `#[buffer]` attribute macro injects a local `__render_result_buffer` +/// variable of type `::mingling::RenderResult` and changes the function's +/// return type to `::mingling::RenderResult`. Inside the body, use the +/// `r_print!` and `r_println!` macros to write into the buffer. +/// +/// # Example +/// +/// ```rust,ignore +/// use mingling::macros::{buffer, r_println}; +/// +/// #[buffer] +/// fn render_my_type(prev: MyType) { +/// r_println!("Value: {:?}", *prev); +/// } +/// ``` +/// +/// This expands to: +/// +/// ```rust,ignore +/// fn render_my_type(prev: MyType) -> mingling::RenderResult { +/// let mut __render_result_buffer = mingling::RenderResult::new(); +/// { +/// r_println!("Value: {:?}", *prev); +/// } +/// __render_result_buffer +/// } +/// ``` +/// +/// # Requirements +/// +/// - The function must return `()` (unit). +/// - The function cannot be async. +#[proc_macro_attribute] +pub fn buffer(attr: TokenStream, item: TokenStream) -> TokenStream { + extensions::buffer::buffer_impl(attr, item) +} + +/// Prints text to a `RenderResult` buffer, with a trailing newline. +/// +/// # Implicit buffer (inside `#[buffer]` functions) +/// +/// ```rust,ignore +/// use mingling::macros::{buffer, r_println}; +/// +/// #[buffer] +/// fn render() { +/// r_println!("Hello, {}!", name); +/// } +/// ``` +/// +/// # Explicit buffer +/// +/// Pass a `RenderResult` variable as the first argument: +/// +/// ```rust,ignore +/// use mingling::macros::r_println; +/// use mingling::RenderResult; +/// +/// let mut r = RenderResult::new(); +/// r_println!(r, "value: {}", 42); +/// assert_eq!(&*r, "value: 42\n"); +/// ``` +#[proc_macro] +pub fn r_println(input: TokenStream) -> TokenStream { + func::r_print::r_println(input) +} + +/// Prints text to a `RenderResult` buffer, without a trailing newline. +/// +/// # Implicit buffer (inside `#[buffer]` functions) +/// +/// ```rust,ignore +/// use mingling::macros::{buffer, r_print}; +/// +/// #[buffer] +/// fn render() { +/// r_print!("Hello, "); +/// r_println!("world!"); +/// } +/// ``` +/// +/// # Explicit buffer +/// +/// ```rust,ignore +/// use mingling::macros::r_print; +/// use mingling::RenderResult; +/// +/// let mut r = RenderResult::new(); +/// r_print!(r, "value: {}", 42); +/// assert_eq!(&*r, "value: 42"); +/// ``` +#[proc_macro] +pub fn r_print(input: TokenStream) -> TokenStream { + func::r_print::r_print(input) +} + /// Derive macro for automatically implementing the `Grouped` trait on a struct. /// /// The `#[derive(Grouped)]` macro: @@ -1441,135 +1598,36 @@ pub fn derive_grouped_serialize(input: TokenStream) -> TokenStream { /// gen_program!(); /// ``` #[proc_macro] -pub fn gen_program(_input: TokenStream) -> TokenStream { - #[cfg(feature = "comp")] - let comp_gen = quote! { - ::mingling::macros::program_comp_gen!(); - }; - - #[cfg(not(feature = "comp"))] - let comp_gen = quote! {}; - - TokenStream::from(quote! { - /// Alias for the current program type `crate::ThisProgram` - pub type Next = ::mingling::ChainProcess<crate::ThisProgram>; - - impl ::mingling::Routable<crate::ThisProgram> for ::mingling::ChainProcess<crate::ThisProgram> - { - fn to_chain(self) -> ::mingling::ChainProcess<crate::ThisProgram> { - match self { - ::mingling::ChainProcess::Ok((any, _)) => { - ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Chain)) - } - other => other, - } - } - - fn to_render(self) -> ::mingling::ChainProcess<crate::ThisProgram> { - match self { - ::mingling::ChainProcess::Ok((any, _)) => { - ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Renderer)) - } - other => other, - } - } - } - - #comp_gen - ::mingling::macros::program_fallback_gen!(); - ::mingling::macros::program_final_gen!(); - }) +pub fn gen_program(input: TokenStream) -> TokenStream { + func::gen_program::gen_program_impl(input) } -/// Internal macro used by `gen_program!` to generate completion infrastructure. +/// Internal macro used by `gen_program!` to generate the completion infrastructure for +/// shell completion support. /// /// **This macro is only available with the `comp` feature.** /// -/// This is an internal macro and should not be called directly by user code. -/// It generates a completion dispatcher, the `CompletionContext` type, and -/// the execution/render logic for shell completion. -/// -/// The generated module `__completion_gen` contains: -/// - A `__comp` dispatcher that routes completion requests -/// - A `__exec_completion` chain that processes `CompletionContext` into `CompletionSuggest` -/// - A `__render_completion` renderer that outputs completion suggestions -#[proc_macro] +/// The `program_comp_gen!` macro generates: +/// 1. A hidden `__internal_completion_mod` module containing: +/// - A `CompletionContext` packed type (wrapping `ShellContext`), dispatched via `"__comp"`. +/// - A `CompletionSuggest` packed type (wrapping `(ShellContext, Suggest)`). +/// - An internal dispatcher (`CMDCompletion`) for the `"__comp"` command path. +/// 2. An internal chain function `__exec_completion` that: +/// - Reads a `ShellContext` from the packed `CompletionContext`. +/// - Calls `CompletionHelper::exec_completion::<ThisProgram>(&ctx)` to generate suggestions. +/// - Routes the result to the completion renderer via `CompletionSuggest`. +/// 3. An internal renderer `__render_completion` that renders the suggestions via +/// `CompletionHelper::render_suggest`. +/// +/// When the `dispatch_tree` feature is enabled, it also imports the internal dispatcher +/// from the generated module into the parent scope for trie-based dispatch. +/// +/// This macro is called automatically by `gen_program!` and should not be called +/// directly by user code. #[cfg(feature = "comp")] -pub fn program_comp_gen(_input: TokenStream) -> TokenStream { - #[cfg(feature = "async")] - let fn_exec_comp = quote! { - #[doc(hidden)] - #[::mingling::macros::chain] - pub async fn __exec_completion(prev: CompletionContext) -> Next { - use ::mingling::Grouped; - - let read_ctx = ::mingling::ShellContext::try_from(prev.inner); - match read_ctx { - Ok(ctx) => { - let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx); - crate::CompletionSuggest::new((ctx, suggest)).to_render() - } - Err(_) => std::process::exit(1), - } - } - }; - - #[cfg(not(feature = "async"))] - let fn_exec_comp = quote! { - #[doc(hidden)] - #[::mingling::macros::chain] - pub fn __exec_completion(prev: CompletionContext) -> Next { - use ::mingling::Grouped; - - let read_ctx = ::mingling::ShellContext::try_from(prev.inner); - match read_ctx { - Ok(ctx) => { - let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx); - crate::CompletionSuggest::new((ctx, suggest)).to_render() - } - Err(_) => std::process::exit(1), - } - } - }; - - #[cfg(feature = "dispatch_tree")] - let internal_dispatcher_comp = quote! { - use __internal_completion_mod::__internal_dispatcher_comp; - }; - - #[cfg(not(feature = "dispatch_tree"))] - let internal_dispatcher_comp = quote! {}; - - let comp_dispatcher = quote! { - #[doc(hidden)] - mod __internal_completion_mod { - use ::mingling::Grouped; - ::mingling::macros::dispatcher!("__comp", CMDCompletion => CompletionContext); - ::mingling::macros::pack!( - CompletionSuggest = (::mingling::ShellContext, ::mingling::Suggest) - ); - } - #internal_dispatcher_comp - use __internal_completion_mod::CompletionContext; - use __internal_completion_mod::CompletionSuggest; - pub use __internal_completion_mod::CMDCompletion; - - #fn_exec_comp - - ::mingling::macros::register_type!(CompletionContext); - - #[allow(unused)] - #[doc(hidden)] - #[::mingling::macros::renderer] - 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 - } - }; - - TokenStream::from(comp_dispatcher) +#[proc_macro] +pub fn program_comp_gen(input: TokenStream) -> TokenStream { + func::gen_program::program_comp_gen_impl(input) } /// Registers a type into the global packed types registry for inclusion in @@ -1594,108 +1652,53 @@ pub fn program_comp_gen(_input: TokenStream) -> TokenStream { /// Panics if the global `PACKED_TYPES` mutex is poisoned. #[proc_macro] pub fn register_type(input: TokenStream) -> TokenStream { - let type_ident = parse_macro_input!(input as syn::Ident); - let entry_str = type_ident.to_string(); - - get_global_set(&PACKED_TYPES) - .lock() - .unwrap() - .insert(entry_str); - - TokenStream::new() + func::gen_program::register_type_impl(input) } -/// Registers a chain mapping from a previous type to a chain struct. +/// Registers a chain mapping function into the global chain registry. /// -/// This macro is called internally by `#[chain]`(macro.chain.html) and is -/// generally not needed in user code. It inserts entries into the global -/// `CHAINS` and `CHAINS_EXIST` registries. +/// This macro is called internally by `#[chain]` and is generally not needed +/// in user code. Each call stores a string entry containing the source-to-target +/// type mapping, which is later consumed by `gen_program!` to generate the +/// `has_chain` and `do_chain` dispatch logic in `ProgramCollect`. /// -/// # Syntax +/// The entry string format is a match arm: the source variant maps to a call +/// that converts the value into a `ChainProcess` via the destination type. /// -/// ```rust,ignore -/// register_chain!(PreviousType, ChainStruct); -/// ``` +/// # Panics /// -/// The `PreviousType` is the input type of the chain step, and `ChainStruct` -/// is the generated struct that implements the `Chain` trait. +/// Panics if the global `CHAINS` mutex is poisoned. #[proc_macro] pub fn register_chain(input: TokenStream) -> TokenStream { - chain::register_chain(input) + func::gen_program::register_chain_impl(input) } -/// Registers a renderer mapping from a type to a renderer struct. +/// Registers a renderer mapping function into the global renderer registry. /// -/// This macro is called internally by `#[renderer]`(macro.renderer.html) and is -/// generally not needed in user code. It inserts entries into the global -/// `RENDERERS`, `RENDERERS_EXIST` and (with `structural_renderer` feature) -/// `STRUCTURAL_RENDERERS` registries. +/// This macro is called internally by `#[renderer]` and is generally not +/// needed in user code. Each call stores a string entry containing the +/// type-to-render mapping, which is later consumed by `gen_program!` to +/// generate the `has_renderer` and `render` dispatch logic in `ProgramCollect`. /// -/// # Syntax +/// The entry string format is a match arm: the type variant maps to a call +/// of the registered renderer function that produces a `RenderResult`. /// -/// ```rust,ignore -/// register_renderer!(PreviousType, RendererStruct); -/// ``` +/// # Panics /// -/// The `PreviousType` is the input type of the renderer, and `RendererStruct` -/// is the generated struct that implements the `Renderer` trait. +/// Panics if the global `RENDERERS` mutex is poisoned. #[proc_macro] pub fn register_renderer(input: TokenStream) -> TokenStream { - renderer::register_renderer(input) + func::gen_program::register_renderer_impl(input) } -/// Internal macro used by `gen_program!` to generate fallback types. -/// -/// This macro generates the fallback wrapper types that are essential -/// for error handling in the Mingling pipeline: -/// -/// - **`ErrorRendererNotFound`** — Wraps a `String` (the name of the missing renderer). -/// Used when no matching renderer is found for a given output type. -/// - **`ErrorDispatcherNotFound`** — Wraps `Vec<String>` (the unrecognized command args). -/// Used when no matching dispatcher is found for user input. -/// - **`ResultEmpty`** — Wraps `()` (the unit type). -/// Used when the chain returns an empty result. -/// -/// Users can (and should) write `#[renderer]` functions for these types -/// to provide meaningful error messages. +/// Internal macro used by `gen_program!` to generate the fallback types for +/// error cases when no dispatcher or renderer is found. /// /// This macro is called automatically by `gen_program!` and should not /// be called directly by user code. -/// -/// # Syntax -/// -/// ```rust,ignore -/// // Called internally by gen_program!: -/// program_fallback_gen!(); -/// ``` -/// -/// # Generated code equivalent -/// -/// ```rust,ignore -/// pack!(ErrorRendererNotFound = String); -/// pack!(ErrorDispatcherNotFound = Vec<String>); -/// pack!(ResultEmpty = ()); -/// ``` #[proc_macro] -pub fn program_fallback_gen(_input: TokenStream) -> TokenStream { - #[cfg(feature = "structural_renderer")] - let pack_empty = quote! { - #[derive(::serde::Serialize, ::mingling::StructuralData, ::mingling::Grouped, Default)] - pub struct ResultEmpty; - }; - - #[cfg(not(feature = "structural_renderer"))] - let pack_empty = quote! { - #[derive(::mingling::Grouped, Default)] - pub struct ResultEmpty; - }; - - let expanded = quote! { - ::mingling::macros::pack!(ErrorRendererNotFound = String); - ::mingling::macros::pack!(ErrorDispatcherNotFound = Vec<String>); - #pack_empty - }; - TokenStream::from(expanded) +pub fn program_fallback_gen(input: TokenStream) -> TokenStream { + func::gen_program::program_fallback_gen_impl(input) } /// Internal macro used by `gen_program!` to generate the final program enum @@ -1747,434 +1750,9 @@ pub fn program_fallback_gen(_input: TokenStream) -> TokenStream { /// pub fn new() -> Program<MyProgram> { Program::new() } /// } /// ``` -/// -/// # Panics -/// -// Feature detection: baked into the proc-macro binary at compile time -#[cfg(feature = "async")] -const ASYNC_ENABLED: bool = true; -#[cfg(not(feature = "async"))] -const ASYNC_ENABLED: bool = false; - -/// Parses an entry of the format `StructName => EnumVariant,` into a pair of idents. -fn parse_entry_pair(entry: &proc_macro2::TokenStream) -> (proc_macro2::Ident, proc_macro2::Ident) { - let s = entry.to_string(); - let arrow_idx = s - .find("=>") - .unwrap_or_else(|| panic!("Entry missing '=>': {s}")); - let struct_str = s[..arrow_idx].trim(); - let variant_str = s[arrow_idx + 2..].trim().trim_end_matches(',').trim(); - let struct_ident = proc_macro2::Ident::new(struct_str, proc_macro2::Span::call_site()); - let variant_ident = proc_macro2::Ident::new(variant_str, proc_macro2::Span::call_site()); - (struct_ident, variant_ident) -} - -/// Loads the pathf type mapping from `$OUT_DIR/{crate}/type_using.rs`. -/// Always compiled; returns empty map when pathf feature is not enabled. -fn load_pathf_map() -> std::collections::HashMap<String, String> { - if !cfg!(feature = "pathf") { - return std::collections::HashMap::new(); - } - let out_dir = std::env::var("OUT_DIR").ok(); - let crate_name = std::env::var("CARGO_PKG_NAME").ok(); - match (out_dir, crate_name) { - (Some(dir), Some(name)) => { - let path = std::path::Path::new(&dir).join(&name).join("type_using.rs"); - match std::fs::read_to_string(&path) { - Ok(content) => content - .lines() - .filter_map(|line| { - let line = line.trim(); - if let Some(rest) = line.strip_prefix("use ") { - let path = rest.strip_suffix(';').unwrap_or(rest); - if let Some((_mod, type_name)) = path.rsplit_once("::") { - return Some((type_name.to_string(), path.to_string())); - } - } - None - }) - .collect(), - Err(_) => std::collections::HashMap::new(), - } - } - _ => std::collections::HashMap::new(), - } -} - -/// Resolves a type name to its full path token stream using the pathf mapping. -pub(crate) fn resolve_type( - name: &str, - map: &std::collections::HashMap<String, String>, -) -> proc_macro2::TokenStream { - if let Some(full_path) = map.get(name) { - syn::parse_str::<proc_macro2::TokenStream>(full_path).unwrap_or_else(|_| { - let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site()); - quote! { #ident } - }) - } else { - let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site()); - quote! { #ident } - } -} - -/// Panics if any of the global registries (`PACKED_TYPES`, `RENDERERS`, `CHAINS`, etc.) -/// are poisoned. #[proc_macro] -#[allow(clippy::too_many_lines)] -pub fn program_final_gen(_input: TokenStream) -> TokenStream { - let name = syn::Ident::new("ThisProgram", proc_macro2::Span::call_site()); - - let packed_types = get_global_set(&PACKED_TYPES).lock().unwrap().clone(); - - let renderers = get_global_set(&RENDERERS).lock().unwrap().clone(); - let chains = get_global_set(&CHAINS).lock().unwrap().clone(); - let renderer_exist = get_global_set(&RENDERERS_EXIST).lock().unwrap().clone(); - let chain_exist = get_global_set(&CHAINS_EXIST).lock().unwrap().clone(); - - #[cfg(feature = "structural_renderer")] - let structural_renderers = get_global_set(&STRUCTURAL_RENDERERS) - .lock() - .unwrap() - .clone(); - - #[cfg(feature = "comp")] - let completions = get_global_set(&COMPLETIONS).lock().unwrap().clone(); - - let packed_types: Vec<proc_macro2::TokenStream> = packed_types - .iter() - .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) - .collect(); - - let renderer_tokens: Vec<proc_macro2::TokenStream> = renderers - .iter() - .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) - .collect(); - - let chain_tokens: Vec<proc_macro2::TokenStream> = chains - .iter() - .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) - .collect(); - - let renderer_exist_tokens: Vec<proc_macro2::TokenStream> = renderer_exist - .iter() - .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) - .collect(); - - let chain_exist_tokens: Vec<proc_macro2::TokenStream> = chain_exist - .iter() - .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) - .collect(); - - let pathf_map: std::collections::HashMap<String, String> = if cfg!(feature = "pathf") { - load_pathf_map() - } else { - std::collections::HashMap::new() - }; - - let pathf_uses: Vec<proc_macro2::TokenStream> = if cfg!(feature = "pathf") { - pathf_map - .values() - .map(|path| format!("use {};", path).parse().unwrap_or_default()) - .collect() - } else { - Vec::new() - }; - - #[cfg(feature = "structural_renderer")] - let structural_renderer_tokens: Vec<proc_macro2::TokenStream> = structural_renderers - .iter() - .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) - .collect(); - - #[cfg(feature = "structural_renderer")] - let structural_render = quote! { - fn structural_render( - any: ::mingling::AnyOutput<Self::Enum>, - setting: &::mingling::StructuralRendererSetting, - ) -> Result<::mingling::RenderResult, ::mingling::error::StructuralRendererSerializeError> { - #[allow(unused_imports)] - #(#pathf_uses)* - match any.member_id { - #(#structural_renderer_tokens)* - _ => { - // Non-structural types: render ResultEmpty (which implements - // StructuralData + Serialize) instead of producing nothing. - let mut r = ::mingling::RenderResult::default(); - ::mingling::StructuralRenderer::render(&ResultEmpty, setting, &mut r)?; - Ok(r) - } - } - } - }; - - #[cfg(not(feature = "structural_renderer"))] - let structural_render = quote! {}; - - #[cfg(feature = "dispatch_tree")] - let compile_time_dispatchers: Vec<String> = get_global_set(&COMPILE_TIME_DISPATCHERS) - .lock() - .unwrap() - .clone() - .iter() - .cloned() - .collect(); - - #[cfg(feature = "dispatch_tree")] - let dispatch_tree_nodes = { - let entries: Vec<(String, String, String)> = compile_time_dispatchers - .iter() - .filter_map(|entry| { - let parts: Vec<&str> = entry.split(':').collect(); - if parts.len() == 3 { - Some(( - parts[0].to_string(), - parts[1].to_string(), - parts[2].to_string(), - )) - } else { - None - } - }) - .collect(); - - let get_nodes_fn = dispatch_tree_gen::gen_get_nodes(&entries, &pathf_map); - let dispatch_trie_fn = dispatch_tree_gen::gen_dispatch_args_trie(&entries, &pathf_map); - - quote! { - #get_nodes_fn - #dispatch_trie_fn - } - }; - - #[cfg(not(feature = "dispatch_tree"))] - let dispatch_tree_nodes = quote! {}; - - #[cfg(feature = "comp")] - let completion_tokens: Vec<proc_macro2::TokenStream> = completions - .iter() - .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) - .collect(); - - #[cfg(feature = "comp")] - let comp = quote! { - fn do_comp(any: &::mingling::AnyOutput<Self::Enum>, ctx: &::mingling::ShellContext) -> ::mingling::Suggest { - #[allow(unused_imports)] - #(#pathf_uses)* - match any.member_id { - #(#completion_tokens)* - _ => ::mingling::Suggest::FileCompletion, - } - } - }; - - #[cfg(not(feature = "comp"))] - 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>) -> ::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); - quote! { - Self::#variant_ident => { - // 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) - } - } - }).collect(); - 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| { - 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); - quote! { - Self::#variant_ident => { - // 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() }; - let fut = async { <#resolved_struct as ::mingling::Chain<Self::Enum>>::proc(value).await }; - ::std::boxed::Box::pin(fut) - } - } - }).collect(); - - let chain_arms_sync: Vec<_> = chain_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); - quote! { - Self::#variant_ident => { - // 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::Chain<Self::Enum>>::proc(value) - } - } - }) - .collect(); - - let do_chain_fn = if chain_tokens.is_empty() { - quote! { - fn do_chain(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::ChainProcess<Self::Enum> { - ::core::panic!("No chain found for type id") - } - } - } else if ASYNC_ENABLED { - quote! { - fn do_chain( - any: ::mingling::AnyOutput<Self::Enum>, - ) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::mingling::ChainProcess<Self::Enum>> + ::std::marker::Send>> { - match any.member_id { - #(#chain_arms_async)* - _ => ::core::panic!("No chain found for type id: {:?}", any.type_id), - } - } - } - } else { - quote! { - fn do_chain( - any: ::mingling::AnyOutput<Self::Enum>, - ) -> ::mingling::ChainProcess<Self::Enum> { - match any.member_id { - #(#chain_arms_sync)* - _ => ::core::panic!("No chain found for type id: {:?}", any.type_id), - } - } - } - }; - - let help_tokens: Vec<proc_macro2::TokenStream> = get_global_set(&HELP_REQUESTS) - .lock() - .unwrap() - .clone() - .iter() - .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap()) - .collect(); - - let num_variants = packed_types.len(); - let repr_type = if u8::try_from(num_variants).is_ok() { - quote! { u8 } - } else if u16::try_from(num_variants).is_ok() { - quote! { u16 } - } else if u32::try_from(num_variants).is_ok() { - quote! { u32 } - } else { - quote! { u128 } - }; - - let expanded = quote! { - #[derive(Debug, PartialEq, Eq, Clone)] - #[repr(#repr_type)] - #[allow(nonstandard_style)] - pub enum #name { - #(#packed_types),* - } - - impl ::std::fmt::Display for #name { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - match self { - #(#name::#packed_types => write!(f, stringify!(#packed_types)),)* - } - } - } - - impl ::mingling::ProgramCollect for #name { - type Enum = #name; - type ErrorDispatcherNotFound = ErrorDispatcherNotFound; - type ErrorRendererNotFound = ErrorRendererNotFound; - type ResultEmpty = ResultEmpty; - fn build_renderer_not_found(member_id: Self::Enum) -> ::mingling::AnyOutput<Self::Enum> { - ::mingling::AnyOutput::new(ErrorRendererNotFound::new(member_id.to_string())) - } - fn build_dispatcher_not_found(args: Vec<String>) -> ::mingling::AnyOutput<Self::Enum> { - ::mingling::AnyOutput::new(ErrorDispatcherNotFound::new(args)) - } - fn build_empty_result() -> ::mingling::AnyOutput<Self::Enum> { - ::mingling::AnyOutput::new(ResultEmpty) - } - #render_fn - #do_chain_fn - 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 { - match any.member_id { - #(#renderer_exist_tokens)* - _ => false - } - } - fn has_chain(any: &::mingling::AnyOutput<Self::Enum>) -> bool { - match any.member_id { - #(#chain_exist_tokens)* - _ => false - } - } - #dispatch_tree_nodes - #structural_render - #comp - } - - impl #name { - /// Creates a new `Program<#name>` instance with default configuration. - pub fn new() -> ::mingling::Program<#name> { - ::mingling::Program::new() - } - - /// Returns a static reference to the global `Program<#name>` singleton. - pub fn this() -> &'static ::mingling::Program<#name> { - &::mingling::this::<#name>() - } - } - }; - - // Clear all global registries to prevent stale state in Rust Analyzer - get_global_set(&PACKED_TYPES).lock().unwrap().clear(); - get_global_set(&CHAINS).lock().unwrap().clear(); - get_global_set(&CHAINS_EXIST).lock().unwrap().clear(); - get_global_set(&RENDERERS).lock().unwrap().clear(); - get_global_set(&RENDERERS_EXIST).lock().unwrap().clear(); - get_global_set(&HELP_REQUESTS).lock().unwrap().clear(); - #[cfg(feature = "comp")] - get_global_set(&COMPLETIONS).lock().unwrap().clear(); - #[cfg(feature = "dispatch_tree")] - get_global_set(&COMPILE_TIME_DISPATCHERS) - .lock() - .unwrap() - .clear(); - #[cfg(feature = "structural_renderer")] - get_global_set(&STRUCTURAL_RENDERERS) - .lock() - .unwrap() - .clear(); - - TokenStream::from(expanded) +pub fn program_final_gen(input: TokenStream) -> TokenStream { + func::gen_program::program_final_gen_impl(input) } /// Builds a `Suggest` instance with inline suggestion items. diff --git a/mingling_macros/src/systems.rs b/mingling_macros/src/systems.rs new file mode 100644 index 0000000..53e58c5 --- /dev/null +++ b/mingling_macros/src/systems.rs @@ -0,0 +1,5 @@ +#[cfg(feature = "dispatch_tree")] +pub(crate) mod dispatch_tree_gen; +pub(crate) mod res_injection; +#[cfg(feature = "structural_renderer")] +pub(crate) mod structural_data; diff --git a/mingling_macros/src/dispatch_tree_gen.rs b/mingling_macros/src/systems/dispatch_tree_gen.rs index b66e2f4..7383421 100644 --- a/mingling_macros/src/dispatch_tree_gen.rs +++ b/mingling_macros/src/systems/dispatch_tree_gen.rs @@ -4,11 +4,11 @@ use just_fmt::snake_case; use proc_macro2::TokenStream; use quote::quote; -use crate::resolve_type; +use crate::func::gen_program::resolve_type; /// Generate the `get_nodes()` function body for a ProgramCollect impl. /// If `pathf_map` is non-empty, resolves internal dispatcher statics using full paths. -pub fn gen_get_nodes( +pub(crate) fn gen_get_nodes( entries: &[(String, String, String)], pathf_map: &HashMap<String, String>, ) -> TokenStream { @@ -40,7 +40,7 @@ pub fn gen_get_nodes( /// Single-node groups use `starts_with`; multi-node groups recurse with `nth()` match. /// /// If `pathf_map` is non-empty, resolves dispatcher types using full paths. -pub fn gen_dispatch_args_trie( +pub(crate) fn gen_dispatch_args_trie( entries: &[(String, String, String)], pathf_map: &HashMap<String, String>, ) -> TokenStream { diff --git a/mingling_macros/src/res_injection.rs b/mingling_macros/src/systems/res_injection.rs index 606b9a6..606b9a6 100644 --- a/mingling_macros/src/res_injection.rs +++ b/mingling_macros/src/systems/res_injection.rs diff --git a/mingling_macros/src/structural_data.rs b/mingling_macros/src/systems/structural_data.rs index 74bcf09..74bcf09 100644 --- a/mingling_macros/src/structural_data.rs +++ b/mingling_macros/src/systems/structural_data.rs diff --git a/mingling_macros/src/utils.rs b/mingling_macros/src/utils.rs new file mode 100644 index 0000000..49d0e7a --- /dev/null +++ b/mingling_macros/src/utils.rs @@ -0,0 +1 @@ +// Shared utilities for the macro crate. diff --git a/mingling_pathf/src/error.rs b/mingling_pathf/src/error.rs index 70a8f6e..d384901 100644 --- a/mingling_pathf/src/error.rs +++ b/mingling_pathf/src/error.rs @@ -22,7 +22,9 @@ pub enum MinglingPathfinderError { /// `parent` is the directory containing the file that declared the module. /// `module_name` is the name of the module that could not be found. ModuleNotFound { + /// The directory containing the file that declared the module. parent: PathBuf, + /// The name of the module that could not be found. module_name: String, }, @@ -30,7 +32,12 @@ 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 { + /// The file containing the invalid `#[path]` attribute. + file: PathBuf, + /// The value of the `#[path]` attribute that points outside the project. + path_attr: String, + }, /// No entry point file (`main.rs`, `lib.rs`, or any file under `bin/`) was found. NoEntryPointFound, @@ -39,7 +46,12 @@ pub enum MinglingPathfinderError { /// /// `path` is the file that failed to parse. /// `message` contains details from the parser. - SynError { path: PathBuf, message: String }, + SynError { + /// The file that failed to parse. + path: PathBuf, + /// Details from the parser about the parse failure. + message: String, + }, } impl fmt::Display for MinglingPathfinderError { diff --git a/mingling_pathf/src/lib.rs b/mingling_pathf/src/lib.rs index 557ae45..69ff3ac 100644 --- a/mingling_pathf/src/lib.rs +++ b/mingling_pathf/src/lib.rs @@ -1,5 +1,6 @@ #![allow(clippy::needless_doctest_main)] #![doc = include_str!("../README.md")] +#![deny(missing_docs)] pub mod config; pub mod error; diff --git a/mingling_pathf/src/pattern_analyzer.rs b/mingling_pathf/src/pattern_analyzer.rs index 5bbc3b4..31d6918 100644 --- a/mingling_pathf/src/pattern_analyzer.rs +++ b/mingling_pathf/src/pattern_analyzer.rs @@ -100,10 +100,12 @@ pub struct PatternAnalyzer { } impl PatternAnalyzer { + /// Creates a new empty `PatternAnalyzer`. pub fn new() -> Self { Self::default() } + /// Registers a new pattern for analysis. pub fn add_pattern(&mut self, pattern: impl AnalyzePattern + 'static) { self.patterns.push(Box::new(pattern)); } diff --git a/mingling_pathf/src/patterns/dispatcher.rs b/mingling_pathf/src/patterns/dispatcher.rs index 6796a2c..eaa53f0 100644 --- a/mingling_pathf/src/patterns/dispatcher.rs +++ b/mingling_pathf/src/patterns/dispatcher.rs @@ -20,10 +20,18 @@ use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; /// - `CMD*` — the dispatcher struct (always) /// - `__internal_dispatcher_*` — dispatch tree static (when `use_dispatch_tree` is true) pub struct DispatcherPattern { + /// Whether the dispatcher generates a dispatch tree static (`__internal_dispatcher_*`). pub use_dispatch_tree: bool, } impl DispatcherPattern { + /// Creates a new `DispatcherPattern`. + /// + /// # Arguments + /// + /// * `use_dispatch_tree` — when `true`, the generated dispatcher also produces a + /// `__internal_dispatcher_*` static dispatch tree item. Set this based on whether + /// your macro invocation includes the `use_dispatch_tree` configuration. pub fn new(use_dispatch_tree: bool) -> Self { Self { use_dispatch_tree } } diff --git a/mingling_pathf/src/patterns/dispatcher_clap.rs b/mingling_pathf/src/patterns/dispatcher_clap.rs index 1a86ad5..0a249b4 100644 --- a/mingling_pathf/src/patterns/dispatcher_clap.rs +++ b/mingling_pathf/src/patterns/dispatcher_clap.rs @@ -29,10 +29,16 @@ use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; /// - `#[dispatcher_clap("greet", CMDGreet, help = true)] struct EntryGreet { ... }` /// - `#[dispatcher_clap("greet", CMDGreet, error = ErrorGreet, help = true)] struct EntryGreet { ... }` pub struct DispatcherClapPattern { + /// Whether to include the `__internal_dispatcher_*` dispatch tree static in the analysis. pub use_dispatch_tree: bool, } impl DispatcherClapPattern { + /// Creates a new `DispatcherClapPattern` with the given configuration. + /// + /// # Parameters + /// - `use_dispatch_tree`: When `true`, enables analysis of the `__internal_dispatcher_*` + /// static dispatch tree for each matched command. pub fn new(use_dispatch_tree: bool) -> Self { Self { use_dispatch_tree } } diff --git a/mling/.gitignore b/mling/.gitignore deleted file mode 100644 index b124b7c..0000000 --- a/mling/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -debug.rs -version.txt diff --git a/mling/Cargo.toml b/mling/Cargo.toml deleted file mode 100644 index 8008463..0000000 --- a/mling/Cargo.toml +++ /dev/null @@ -1,41 +0,0 @@ -[package] -name = "mingling-cli" -version.workspace = true -edition.workspace = true -authors = ["Weicao-CatilGrass"] -license.workspace = true -readme = "README.md" -repository.workspace = true -description = "Mingling's scaffolding tool" -keywords = ["cli", "scaffolding", "command-line", "framework"] -categories = ["command-line-interface"] - -[[bin]] -name = "cargo-mling" -path = "src/bin/cargo-mling.rs" - -[[bin]] -name = "mling" -path = "src/bin/mling.rs" - -[dependencies] -mingling = { path = "../mingling", features = [ - "parser", - "comp", - "structural_renderer", - "extra_macros", - "yaml_serde_fmt", - "pathf" -] } - -serde = { version = "1", features = ["derive"] } -serde_json = "1" - -colored = "3.1.1" -dirs = "6.0.0" -just_fmt = "0.1.2" -toml.workspace = true - -[build-dependencies] -chrono = "0.4" -mingling = { path = "../mingling", features = ["comp", "pathf", "builds"] } diff --git a/mling/LICENSE-APACHE b/mling/LICENSE-APACHE deleted file mode 120000 index 965b606..0000000 --- a/mling/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../LICENSE-APACHE
\ No newline at end of file diff --git a/mling/LICENSE-MIT b/mling/LICENSE-MIT deleted file mode 120000 index 76219eb..0000000 --- a/mling/LICENSE-MIT +++ /dev/null @@ -1 +0,0 @@ -../LICENSE-MIT
\ No newline at end of file diff --git a/mling/README.md b/mling/README.md deleted file mode 100644 index a4d0b1d..0000000 --- a/mling/README.md +++ /dev/null @@ -1,3 +0,0 @@ -This directory is the companion CLI tool for Mingling. After installation, it can be invoked via `cargo mling` or `mling`. - -It is currently under refactoring and **completely unusable**. diff --git a/mling/build.rs b/mling/build.rs deleted file mode 100644 index 9a7b503..0000000 --- a/mling/build.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::path::Path; -use std::process::Command; - -use mingling::build::analyze_and_build_type_mapping; -use mingling::build::build_comp_scripts; - -fn main() { - build_version_info(); - build_completion(); - analyze_and_build_type_mapping().unwrap(); -} - -fn build_version_info() { - // Read version from CARGO_PKG_VERSION (inherited from workspace Cargo.toml) - let version = env!("CARGO_PKG_VERSION"); - - // Get git commit hash (first 9 characters) - let commit_hash = Command::new("git") - .args(["rev-parse", "--short=9", "HEAD"]) - .output() - .ok() - .and_then(|output| { - if output.status.success() { - String::from_utf8(output.stdout).ok() - } else { - None - } - }) - .map(|s| s.trim().to_string()) - .unwrap_or_else(|| "unknown".to_string()); - - // Get date from git commit, fallback to current date - let date = Command::new("git") - .args(["log", "-1", "--format=%ad", "--date=format:%Y-%m-%d"]) - .output() - .ok() - .and_then(|output| { - if output.status.success() { - String::from_utf8(output.stdout).ok() - } else { - None - } - }) - .map(|s| s.trim().to_string()) - .unwrap_or_else(|| chrono::Local::now().format("%Y-%m-%d").to_string()); - - let version_string = format!("mling {version} ({commit_hash} {date})"); - - let out_dir = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()) - .join("src") - .join("helps") - .join("version.txt"); - - std::fs::write(&out_dir, version_string).expect("failed to write version.txt"); - - println!("cargo::rerun-if-changed=../Cargo.toml"); - println!("cargo::rerun-if-changed=../Cargo.lock"); - println!("cargo::rerun-if-changed=.git/HEAD"); -} - -fn build_completion() { - build_comp_scripts("mling").unwrap(); -} diff --git a/mling/res/CHECKLIST.md b/mling/res/CHECKLIST.md deleted file mode 100644 index bc7b8bc..0000000 --- a/mling/res/CHECKLIST.md +++ /dev/null @@ -1,110 +0,0 @@ -> [!NOTE] -> -> You are using `mling` to create your `mingling` project. Before proceeding, please fill in your project information. - -## Question 1: Fill in your project information - -Use `snake-case` style to fill in the project name. It will serve as your `crate` name: - -```name -my-cli -``` - -Describe your `program` in one sentence: - -```description -This is a command-line program -``` - -## Question 2: Which Mingling version do you need? - -> [!TIP] -> -> You must select **EXACTLY ONE** option - -- [x] `ver:0.2` Based on the `Mingling@0.2.x` template - -## Question 3: What is your project layout style? - -> [!TIP] -> -> You must select **EXACTLY ONE** option - -- [x] `base:flat` Flat mode (Simple) - -``` -# The most basic mode -./main.rs -./foo.rs -./bar.rs -./globals.rs -``` - -- [ ] `base:modularity` Modular mode - -``` -├─ module1/ # Stores private concepts internally -│ ├── mod.rs # Command registration -│ ├── chains/ # Core logic -│ └── renderers/ # Result rendering -├─ module2/ -│ ├── mod.rs # Command registration -│ ├── chains/ # Core logic -│ └── renderers/ # Result rendering -├─ global/ # Stores public concepts in the global directory -│ ├── errors/ # Global Errors -│ ├── setups/ # Setups -│ └── resources/ # Global Resources -└─ main.rs -``` - -- [ ] `base:command` Command mode - -``` -# Stores all commands -├─ commands/ # Organizes subcommands by hierarchy -│ ├── remote/add.rs # "remote add" -│ ├── remote/rm.rs # "remote rm" -│ ├── remote.rs # Not a command, -│ │ but a local implementation for -│ │ the remote series of commands -│ ├── foo.rs # "foo" -│ └── bar.rs # "bar" -├─ errors/ # Global Errors -├─ resources/ # Global Resources -├─ setups/ # Setups -└─ main.rs -``` - -## Question 4: What additional features do you need to enable? - -> [!TIP] -> -> All of the following options are **OPTIONAL** - -**Pre:** - -- [x] `exec:git_init` Initialize this project as a git repository (requires `git` to be installed) - -**Features:** - -- [x] `feat:extra_macros` Extra macro support -- [x] `feat:dispatch_tree` Compile-time dispatch tree (solidify all commands) -- [ ] `feat:structural_renderer` Structural renderer (JSON/YAML) for feature output -- [ ] `feat:comp` Dynamic completion system -- [ ] `feat:clap` Clap support -- [ ] `feat:parser` Mingling Picker parser support -- [ ] `feat:async` Async support - -**Implementations:** - -- [ ] `impl:exit_code` Introduce exit code control -- [ ] `impl:fallback` Fallback functionality when no command is given - ---- - -After filling in the above, execute the following command to create the project: - -``` -~# mling gen -``` diff --git a/mling/res/template_0.2/command/Cargo.toml.tmpl b/mling/res/template_0.2/command/Cargo.toml.tmpl deleted file mode 100644 index e69de29..0000000 --- a/mling/res/template_0.2/command/Cargo.toml.tmpl +++ /dev/null diff --git a/mling/res/template_0.2/command/src/main.rs b/mling/res/template_0.2/command/src/main.rs deleted file mode 100644 index 8b13789..0000000 --- a/mling/res/template_0.2/command/src/main.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/mling/res/template_0.2/flat/Cargo.toml.tmpl b/mling/res/template_0.2/flat/Cargo.toml.tmpl deleted file mode 100644 index e69de29..0000000 --- a/mling/res/template_0.2/flat/Cargo.toml.tmpl +++ /dev/null diff --git a/mling/res/template_0.2/flat/src/main.rs b/mling/res/template_0.2/flat/src/main.rs deleted file mode 100644 index e69de29..0000000 --- a/mling/res/template_0.2/flat/src/main.rs +++ /dev/null diff --git a/mling/res/template_0.2/modularity/Cargo.toml.tmpl b/mling/res/template_0.2/modularity/Cargo.toml.tmpl deleted file mode 100644 index e69de29..0000000 --- a/mling/res/template_0.2/modularity/Cargo.toml.tmpl +++ /dev/null diff --git a/mling/res/template_0.2/modularity/src/main.rs b/mling/res/template_0.2/modularity/src/main.rs deleted file mode 100644 index e69de29..0000000 --- a/mling/res/template_0.2/modularity/src/main.rs +++ /dev/null diff --git a/mling/src/bin/cargo-mling.rs b/mling/src/bin/cargo-mling.rs deleted file mode 100644 index 4c285c6..0000000 --- a/mling/src/bin/cargo-mling.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - mingling_cli::cli::run(); -} diff --git a/mling/src/bin/mling.rs b/mling/src/bin/mling.rs deleted file mode 100644 index 4c285c6..0000000 --- a/mling/src/bin/mling.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - mingling_cli::cli::run(); -} diff --git a/mling/src/cargo_style.rs b/mling/src/cargo_style.rs deleted file mode 100644 index c212094..0000000 --- a/mling/src/cargo_style.rs +++ /dev/null @@ -1,221 +0,0 @@ -use colored::Colorize; - -/// Formats a message in cargo-style format with a bold green prefix. -/// -/// The message should be in the format `prefix: content`. The prefix will be -/// bold green and right-padded to 12 characters. If there is no colon in the -/// string, the entire string is printed as content with an empty prefix. -/// -/// # Macros -/// -/// - `format_cargo!("prefix: {}", arg)` — format-style invocation -/// - `format_cargo!(expr)` — direct expression invocation -/// -/// # Panics -/// -/// Panics if the prefix (text before the first `:`) exceeds 12 characters. -/// -/// # Examples -/// -/// ```ignore -/// format_cargo!("Compiling: hello.rs"); -/// // Output: " Compiling hello.rs" (green bold "Compiling" padded to 12) -/// ``` -#[macro_export] -macro_rules! format_cargo { - ($fmt:literal, $($arg:tt)*) => { - $crate::get_cargo_info_format(format!($fmt, $($arg)*)) - }; - ($cmd:expr) => { - $crate::get_cargo_info_format($cmd) - }; -} - -/// Formats an error message in cargo-style format with a bold red "error" prefix. -/// -/// # Macros -/// -/// - `eformat_cargo!("prefix: {}", arg)` — format-style invocation -/// - `eformat_cargo!(expr)` — direct expression invocation -/// -/// # Examples -/// -/// ```ignore -/// eformat_cargo!("failed to parse input"); -/// // Output: "error: failed to parse input" (red bold "error") -/// ``` -#[macro_export] -macro_rules! eformat_cargo { - ($fmt:literal, $($arg:tt)*) => { - $crate::get_cargo_error_format(format!($fmt, $($arg)*)) - }; - ($cmd:expr) => { - $crate::get_cargo_error_format($cmd) - }; -} - -/// Formats a help message in cargo-style format with a bright white "help" prefix. -/// -/// # Macros -/// -/// - `hformat_cargo!("prefix: {}", arg)` — format-style invocation -/// - `hformat_cargo!(expr)` — direct expression invocation -/// -/// # Examples -/// -/// ```ignore -/// hformat_cargo!("use --verbose for more info"); -/// // Output: "help: use --verbose for more info" (bright white "help") -/// ``` -#[macro_export] -macro_rules! hformat_cargo { - ($fmt:literal, $($arg:tt)*) => { - $crate::get_cargo_help_format(format!($fmt, $($arg)*)) - }; - ($cmd:expr) => { - $crate::get_cargo_help_format($cmd) - }; -} - -/// Print a message in cargo-style format with a bold green prefix. -/// -/// # Macros -/// -/// - `println_cargo!("prefix: {}", arg)` — format-style invocation -/// - `println_cargo!(expr)` — direct expression invocation -/// -/// # Examples -/// -/// ```ignore -/// println_cargo!("Compiling: hello.rs"); -/// ``` -#[macro_export] -macro_rules! println_cargo { - ($fmt:literal, $($arg:tt)*) => { - println!("{}", $crate::get_cargo_info_format(format!($fmt, $($arg)*))) - }; - ($cmd:expr) => { - println!("{}", $crate::get_cargo_info_format($cmd)) - }; -} - -/// Print an error message in cargo-style format with a bold red "error" prefix. -/// -/// # Macros -/// -/// - `eprintln_cargo!("prefix: {}", arg)` — format-style invocation -/// - `eprintln_cargo!(expr)` — direct expression invocation -/// -/// # Examples -/// -/// ```ignore -/// eprintln_cargo!("failed to parse input"); -/// ``` -#[macro_export] -macro_rules! eprintln_cargo { - ($fmt:literal, $($arg:tt)*) => { - eprintln!("{}", $crate::get_cargo_error_format(format!($fmt, $($arg)*))) - }; - ($cmd:expr) => { - eprintln!("{}", $crate::get_cargo_error_format($cmd)) - }; -} - -/// Print a help message in cargo-style format with a bright white "help" prefix. -/// -/// # Macros -/// -/// - `hprintln_cargo!("prefix: {}", arg)` — format-style invocation -/// - `hprintln_cargo!(expr)` — direct expression invocation -/// -/// # Examples -/// -/// ```ignore -/// hprintln_cargo!("use --verbose for more info"); -/// ``` -#[macro_export] -macro_rules! hprintln_cargo { - ($fmt:literal, $($arg:tt)*) => { - println!("{}", $crate::get_cargo_help_format(format!($fmt, $($arg)*))) - }; - ($cmd:expr) => { - println!("{}", $crate::get_cargo_help_format($cmd)) - }; -} - -/// Format a message in cargo style format, with bold green prefix. -/// -/// The input string is split at the first `:`. The part before the colon becomes -/// the prefix (bold green, right-padded to 12 characters), and the part after -/// becomes the content. -/// -/// If no colon is found, the entire string is treated as content and no prefix -/// is shown. -/// -/// # Panics -/// -/// Panics if the prefix (text before the first `:`) exceeds 12 characters. -/// -/// # Examples -/// -/// ```ignore -/// get_cargo_info_format("Compiling: my_program.rs"); -/// // returns " Compiling my_program.rs" -/// ``` -pub fn get_cargo_info_format(str: impl Into<String>) -> String { - let s = str.into(); - let (prefix, content) = if let Some(pos) = s.find(':') { - ( - s[..pos].trim().to_string(), - s[pos + 1..].trim_start().to_string(), - ) - } else { - (String::new(), s.trim().to_string()) - }; - - assert!( - prefix.len() <= 12, - "prefix length exceeds 12: '{}' has length {}", - prefix, - prefix.len() - ); - - let padding = " ".repeat(12 - prefix.len()); - - format!( - "{}{} {}", - padding, - prefix.bold().bright_green(), - content.trim() - ) -} - -/// Format an error message in cargo style format, with bold red "error" prefix. -/// -/// The input string is printed as the error content, prefixed by a bold red -/// `error:` label. -/// -/// # Examples -/// -/// ```ignore -/// get_cargo_error_format("something went wrong"); -/// // returns "error: something went wrong" -/// ``` -pub fn get_cargo_error_format(str: impl Into<String>) -> String { - format!("{}: {}", "error".bold().bright_red(), str.into()) -} - -/// Format a help message in cargo style format, with bright white "help" prefix. -/// -/// The input string is printed as the help content, prefixed by a bright white -/// `help:` label (not bold). -/// -/// # Examples -/// -/// ```ignore -/// get_cargo_help_format("use --verbose for more info"); -/// // returns "help: use --verbose for more info" -/// ``` -pub fn get_cargo_help_format(str: impl Into<String>) -> String { - format!("{}: {}", "help".bright_white(), str.into()) -} diff --git a/mling/src/cli.rs b/mling/src/cli.rs deleted file mode 100644 index 69e4fe9..0000000 --- a/mling/src/cli.rs +++ /dev/null @@ -1,198 +0,0 @@ -// `mling` has not been replaced from `parser` to `picker` yet -#![allow(deprecated)] - -use crate::{ - CMDCompletion, ErrorDispatcherNotFound, Next, ThisProgram, - display::markdown, - eformat_cargo, eprintln_cargo, hformat_cargo, - pkg_mgr::{CMDInstall, CMDListNamespace, CMDRemoveNamespace, PackageManagerSetup}, - proj_mgr::ProjectManagerSetup, - res::{ResCurrentDir, ResManifestPath}, -}; -use colored::Colorize; -use mingling::{ - Grouped, Program, RenderResult, - hook::ProgramHook, - macros::{chain, help, pack, program_setup, renderer}, - res::ResExitCode, - setup::{ExitCodeSetup, HelpFlagSetup, QuietFlagSetup, StructuralRendererSetup}, -}; -use std::{env::current_dir, io::Write, path::PathBuf, process::exit, str::FromStr}; - -pub fn run() { - #[cfg(windows)] - colored::control::set_virtual_terminal(true).unwrap(); - - // Preprocess args to handle cargo-mling invocations - let mut args: Vec<String> = std::env::args().collect(); - if args.first().is_some_and(|a| a.contains("cargo-mling")) { - args[0] = "cargo-mling".to_string(); - } - if args.get(1).is_some_and(|a| a == "mling") { - args.remove(1); - } - - // Build program with preprocessed args - let mut program = Program::<ThisProgram>::new_with_args(args); - - // Intercept Version - program.global_flag(["-V", "--version"], |_| { - eprintln!(include_str!("helps/version.txt")); - exit(0) - }); - - // Intercept Help - program.with_hook( - ProgramHook::empty().on_post_dispatch(|info| match info.entry { - // When dispatcher is not found - ThisProgram::ErrorDispatcherNotFound - // And user requests Help - if ThisProgram::this().user_context.help => { - // Print help - eprintln!("{}", markdown(include_str!("helps/mling_help.txt"))); - exit(0) - } - _ => {} - }), - ); - - // Commands - program.with_dispatcher(CMDCompletion); - - // Setups - program.with_setup(HelpFlagSetup::new(["-h", "--help"])); - program.with_setup(StructuralRendererSetup); - program.with_setup(ExitCodeSetup::default()); - - program.with_setup(StandardOutputSetup); - program.with_setup(PackageManagerSetup); - program.with_setup(ProjectManagerSetup); - - // Resources - program.with_resource(ResCurrentDir { - path: current_dir().unwrap(), - }); - - let manifest_path = program.pick_global_argument(["-P", "--manifest-path"]); - program.with_resource(ResManifestPath { - raw: manifest_path, - resolved: None, - }); - - // Manifest Path Check - if !program.is_completing() { - program.with_hook(ProgramHook::empty().on_post_dispatch(|_| { - let p = ThisProgram::this(); - p.modify_res(|manifest_path: &mut ResManifestPath| { - manifest_path.resolved = Some(resolve_manifest_path(manifest_path.raw.clone())); - }); - })); - } - - // Execute - let quiet = program.stdout_setting.quiet; - let error_output = program.stdout_setting.error_output && !quiet; - let render_output = program.stdout_setting.render_output && !quiet; - let result = program.exec_without_render().unwrap(); - if !result.is_empty() { - if result.exit_code == 0 && render_output { - println!("{}", result.trim()); - } else if error_output { - eprintln!("{}", result.trim()); - } - } - exit(result.exit_code); -} - -#[program_setup] -fn standard_output_setup(program: &mut Program<ThisProgram>) { - program.with_setup(QuietFlagSetup::new("--silence")); - program.global_flag(["--no-error"], |program| { - program.stdout_setting.error_output = false; - }); - program.global_flag(["--no-result"], |program| { - program.stdout_setting.render_output = false; - }); - program.global_flag(["--silence", "--quiet"], |program| { - program.stdout_setting.quiet = true; - }); -} - -fn resolve_manifest_path(provided: Option<String>) -> PathBuf { - let p = ThisProgram::this(); - let disable_error_output = - // --no-error - !p.stdout_setting.error_output || - // or --quiet/--silence - p.stdout_setting.quiet; - - if let Some(path) = provided { - let p = PathBuf::from_str(&path).unwrap(); - if p.is_dir() { - let candidate = p.join("Cargo.toml"); - if candidate.exists() { - return candidate; - } - if !disable_error_output { - eprintln_cargo!("`{}` is not a crate root", p.display()); - } - exit(1); - } - return p; - } - // Walk up from current directory to find nearest Cargo.toml - let mut dir = current_dir().unwrap(); - loop { - let candidate = dir.join("Cargo.toml"); - if candidate.exists() { - return candidate; - } - if !dir.pop() { - // Reached filesystem root without finding Cargo.toml - if !disable_error_output { - eprintln_cargo!("`{}` is not a crate root", current_dir().unwrap().display()); - } - exit(1); - } - } -} - -pack!(ResultMlingHelp = ()); -pack!(ResultUnknownCommand = String); - -#[chain] -pub fn handle_error_dispatcher_not_found(err: ErrorDispatcherNotFound) -> Next { - if err.is_empty() { - ResultMlingHelp::default().to_render() - } else { - ResultUnknownCommand::new(err.join(" ")).to_render() - } -} - -#[renderer] -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) -> RenderResult { - let mut result = RenderResult::default(); - writeln!( - result, - "{}", - eformat_cargo!("no such command: `{}`", prev.bright_yellow().bold()) - ) - .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/display.rs b/mling/src/display.rs deleted file mode 100644 index 313a054..0000000 --- a/mling/src/display.rs +++ /dev/null @@ -1,384 +0,0 @@ -use colored::Colorize; -use std::collections::VecDeque; - -/// Trait for adding markdown formatting to strings -pub trait Markdown { - fn markdown(&self) -> String; -} - -impl Markdown for &str { - fn markdown(&self) -> String { - markdown(self) - } -} - -impl Markdown for String { - fn markdown(&self) -> String { - markdown(self) - } -} - -/// Converts a string to colored/formatted text with ANSI escape codes. -/// -/// Supported syntax: -/// - Bold: `**text**` -/// - Italic: `*text*` -/// - Underline: `_text_` -/// - Angle-bracketed content: `<text>` (displayed as cyan) -/// - Inline code: `` `text` `` (displayed as green) -/// - Color tags: `[[color_name]]` and `[[/]]` to close color -/// - Escape characters: `\*`, `\<`, `\>`, `` \` ``, `\_` for literal characters -/// - Headings: `# Heading 1`, `## Heading 2`, up to `###### Heading 6` -/// - Blockquote: `> text` (displays a gray background marker at the beginning of the line) -/// -/// Color tags support the following color names: -/// Color tags support the following color names: -/// -/// | Type | Color Names | -/// |-------------------------|-----------------------------------------------------------------------------| -/// | Standard colors | `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white` | -/// | Bright colors | `bright_black` | -/// | | `bright_red` | -/// | | `bright_green` | -/// | | `bright_yellow` | -/// | | `bright_blue` | -/// | | `bright_magenta` | -/// | | `bright_cyan` | -/// | | `bright_white` | -/// | Bright color shorthands | `b_black` | -/// | | `b_red` | -/// | | `b_green` | -/// | | `b_yellow` | -/// | | `b_blue` | -/// | | `b_magenta` | -/// | | `b_cyan` | -/// | | `b_white` | -/// | Gray colors | `gray`/`grey` | -/// | | `bright_gray`/`bright_grey` | -/// | | `b_gray`/`b_grey` | -/// -/// Color tags can be nested, `[[/]]` will close the most recently opened color tag. -/// -/// # Arguments -/// * `text` - The text to format, can be any type that implements `AsRef<str>` -/// -/// # Returns -/// Returns a `String` containing ANSI escape codes that can display colored/formatted text in ANSI-supported terminals. -/// -/// # Examples -/// ``` -/// # use mingling_cli::display::markdown; -/// let formatted = markdown("Hello **world**!"); -/// println!("{}", formatted); -/// -/// let colored = markdown("[[red]]Red text[[/]] and normal text"); -/// println!("{}", colored); -/// -/// let nested = markdown("[[blue]]Blue [[green]]Green[[/]] Blue[[/]] normal"); -/// println!("{}", nested); -/// ``` -pub fn markdown(text: impl AsRef<str>) -> String { - let text = text.as_ref(); - let lines: Vec<&str> = text.lines().collect(); - let mut result = String::new(); - let mut content_indent = 0; - - for line in lines { - // Don't trim the line initially, we need to check if it starts with # - let trimmed_line = line.trim(); - let mut line_result = String::new(); - - // Check if line starts with # for heading - // Check if the original line (not trimmed) starts with # - if line.trim_start().starts_with('#') { - let chars: Vec<char> = line.trim_start().chars().collect(); - let mut level = 0; - - // Count # characters at the beginning - while level < chars.len() && level < 7 && chars[level] == '#' { - level += 1; - } - - // Cap level at 6 - let effective_level = if level > 6 { 6 } else { level }; - - // Skip # characters and any whitespace after them - let mut content_start = level; - while content_start < chars.len() && chars[content_start].is_whitespace() { - content_start += 1; - } - - // Extract heading content - let heading_content: String = if content_start < chars.len() { - chars[content_start..].iter().collect() - } else { - String::new() - }; - - // Process the heading content with formatting - let processed_content = process_line(&heading_content); - - // Format heading as white background, black text, bold - // ANSI codes: \x1b[1m for bold, \x1b[47m for white background, \x1b[30m for black text - let formatted_heading = format!("\x1b[1m\x1b[47m\x1b[30m {processed_content} \x1b[0m"); - - // Add indentation to the heading line itself - // Heading indentation = level - 1 - let heading_indent = if effective_level > 0 { - effective_level - 1 - } else { - 0 - }; - let indent = " ".repeat(heading_indent); - line_result.push_str(&indent); - line_result.push_str(&formatted_heading); - - // Update content indent level for subsequent content - // Content after heading should be indented by effective_level - content_indent = effective_level; - } else if !trimmed_line.is_empty() { - // Process regular line with existing formatting - let processed_line = process_line_with_quote(trimmed_line); - - // Add indentation based on content_indent - let indent = " ".repeat(content_indent); - line_result.push_str(&indent); - line_result.push_str(&processed_line); - } else { - line_result.push(' '); - } - - if !line_result.is_empty() { - result.push_str(&line_result); - result.push('\n'); - } - } - - // Remove trailing newline - if result.ends_with('\n') { - result.pop(); - } - - result -} - -// Helper function to process a single line with existing formatting and handle > quotes -fn process_line_with_quote(line: &str) -> String { - let chars: Vec<char> = line.chars().collect(); - - // Check if line starts with '>' and not escaped - if !chars.is_empty() && chars[0] == '>' { - // Check if it's escaped - if chars.len() > 1 && chars[1] == '\\' { - // It's \>, so treat as normal text starting from position 0 - return process_line(line); - } - - // It's a regular > at the beginning, replace with gray background gray text space - let gray_bg_space = "\x1b[48;5;242m\x1b[38;5;242m \x1b[0m"; - let rest_of_line = if chars.len() > 1 { - chars[1..].iter().collect::<String>() - } else { - String::new() - }; - - // Process the rest of the line normally - let processed_rest = process_line(&rest_of_line); - - // Combine the gray background space with the processed rest - format!("{gray_bg_space}{processed_rest}") - } else { - // No > at the beginning, process normally - process_line(line) - } -} - -// Helper function to process a single line with existing formatting -fn process_line(line: &str) -> String { - let mut result = String::new(); - let mut color_stack: VecDeque<String> = VecDeque::new(); - - let chars: Vec<char> = line.chars().collect(); - let mut i = 0; - - while i < chars.len() { - // Check for escape character \ - if chars[i] == '\\' && i + 1 < chars.len() { - let escaped_char = chars[i + 1]; - // Only escape specific characters - if matches!(escaped_char, '*' | '<' | '>' | '`' | '_') { - let mut escaped_text = escaped_char.to_string(); - apply_color_stack(&mut escaped_text, &color_stack); - result.push_str(&escaped_text); - i += 2; - continue; - } - } - - // Check for color tag start [[color]] - if i + 1 < chars.len() - && chars[i] == '[' - && chars[i + 1] == '[' - && let Some(end) = find_tag_end(&chars, i) - { - let tag_content: String = chars[i + 2..end].iter().collect(); - - // Check if it's a closing tag [[/]] - if tag_content == "/" { - color_stack.pop_back(); - } else { - // It's a color tag - color_stack.push_back(tag_content.clone()); - } - i = end + 2; - continue; - } - - // Check for bold **text** - if i + 1 < chars.len() - && chars[i] == '*' - && chars[i + 1] == '*' - && let Some(end) = find_matching(&chars, i + 2, "**") - { - let bold_text: String = chars[i + 2..end].iter().collect(); - let mut formatted_text = bold_text.bold().to_string(); - apply_color_stack(&mut formatted_text, &color_stack); - result.push_str(&formatted_text); - i = end + 2; - continue; - } - - // Check for italic *text* - if chars[i] == '*' - && let Some(end) = find_matching(&chars, i + 1, "*") - { - let italic_text: String = chars[i + 1..end].iter().collect(); - let mut formatted_text = italic_text.italic().to_string(); - apply_color_stack(&mut formatted_text, &color_stack); - result.push_str(&formatted_text); - i = end + 1; - continue; - } - - // Check for underline _text_ - if chars[i] == '_' - && let Some(end) = find_matching(&chars, i + 1, "_") - { - let underline_text: String = chars[i + 1..end].iter().collect(); - let mut formatted_text = format!("\x1b[4m{underline_text}\x1b[0m"); - apply_color_stack(&mut formatted_text, &color_stack); - result.push_str(&formatted_text); - i = end + 1; - continue; - } - - // Check for angle-bracketed content <text> - if chars[i] == '<' - && let Some(end) = find_matching(&chars, i + 1, ">") - { - // Include the angle brackets in the output - let angle_text: String = chars[i..=end].iter().collect(); - let mut formatted_text = angle_text.cyan().to_string(); - apply_color_stack(&mut formatted_text, &color_stack); - result.push_str(&formatted_text); - i = end + 1; - continue; - } - - // Check for inline code `text` - if chars[i] == '`' - && let Some(end) = find_matching(&chars, i + 1, "`") - { - // Include the backticks in the output - let code_text: String = chars[i..=end].iter().collect(); - let mut formatted_text = code_text.green().to_string(); - apply_color_stack(&mut formatted_text, &color_stack); - result.push_str(&formatted_text); - i = end + 1; - continue; - } - - // Regular character - let mut current_char = chars[i].to_string(); - apply_color_stack(&mut current_char, &color_stack); - result.push_str(¤t_char); - i += 1; - } - - result -} - -// Helper function to find matching delimiter -fn find_matching(chars: &[char], start: usize, delimiter: &str) -> Option<usize> { - let delim_chars: Vec<char> = delimiter.chars().collect(); - let delim_len = delim_chars.len(); - - let mut j = start; - while j < chars.len() { - if delim_len == 1 { - if chars[j] == delim_chars[0] { - return Some(j); - } - } else if j + 1 < chars.len() - && chars[j] == delim_chars[0] - && chars[j + 1] == delim_chars[1] - { - return Some(j); - } - j += 1; - } - None -} - -// Helper function to find color tag end -fn find_tag_end(chars: &[char], start: usize) -> Option<usize> { - let mut j = start + 2; - while j + 1 < chars.len() { - if chars[j] == ']' && chars[j + 1] == ']' { - return Some(j); - } - j += 1; - } - None -} - -// Helper function to apply color stack to text -fn apply_color_stack(text: &mut String, color_stack: &VecDeque<String>) { - let mut result = text.clone(); - for color in color_stack.iter().rev() { - result = apply_color(&result, color); - } - *text = result; -} - -// Helper function to apply color to text -fn apply_color(text: impl AsRef<str>, color_name: impl AsRef<str>) -> String { - let text = text.as_ref(); - let color_name = color_name.as_ref(); - match color_name { - // Normal colors - "black" => text.black().to_string(), - "red" => text.red().to_string(), - "green" => text.green().to_string(), - "yellow" => text.yellow().to_string(), - "blue" => text.blue().to_string(), - "magenta" => text.magenta().to_string(), - "cyan" => text.cyan().to_string(), - "white" | "b_white" | "bright_gray" | "bright_grey" | "b_gray" | "b_grey" => { - text.white().to_string() - } - - // Bright colors and their b_ short aliases - "bright_black" | "b_black" | "gray" | "grey" => text.bright_black().to_string(), - "bright_red" | "b_red" => text.bright_red().to_string(), - "bright_green" | "b_green" => text.bright_green().to_string(), - "bright_yellow" | "b_yellow" => text.bright_yellow().to_string(), - "bright_blue" | "b_blue" => text.bright_blue().to_string(), - "bright_magenta" | "b_magenta" => text.bright_magenta().to_string(), - "bright_cyan" | "b_cyan" => text.bright_cyan().to_string(), - "bright_white" => text.bright_white().to_string(), - - // Default to white if color not recognized - _ => text.to_string(), - } -} diff --git a/mling/src/errors.rs b/mling/src/errors.rs deleted file mode 100644 index ece80ce..0000000 --- a/mling/src/errors.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod io_error; -pub use io_error::*; diff --git a/mling/src/errors/io_error.rs b/mling/src/errors/io_error.rs deleted file mode 100644 index ac503cc..0000000 --- a/mling/src/errors/io_error.rs +++ /dev/null @@ -1,224 +0,0 @@ -use mingling::{ - RenderResult, - macros::{group, renderer}, - res::ResExitCode, -}; -use std::io::Write as _; - -use crate::eformat_cargo; - -group!(ErrorIo = std::io::Error); - -// Error code constants for each std::io::ErrorKind variant -pub const EC_IO_ERR_NOT_FOUND: i32 = 1000; -pub const EC_IO_ERR_PERMISSION_DENIED: i32 = 1001; -pub const EC_IO_ERR_CONNECTION_REFUSED: i32 = 1002; -pub const EC_IO_ERR_CONNECTION_RESET: i32 = 1003; -pub const EC_IO_ERR_HOST_UNREACHABLE: i32 = 1004; -pub const EC_IO_ERR_NETWORK_UNREACHABLE: i32 = 1005; -pub const EC_IO_ERR_CONNECTION_ABORTED: i32 = 1006; -pub const EC_IO_ERR_NOT_CONNECTED: i32 = 1007; -pub const EC_IO_ERR_ADDR_IN_USE: i32 = 1008; -pub const EC_IO_ERR_ADDR_NOT_AVAILABLE: i32 = 1009; -pub const EC_IO_ERR_NETWORK_DOWN: i32 = 1010; -pub const EC_IO_ERR_BROKEN_PIPE: i32 = 1011; -pub const EC_IO_ERR_ALREADY_EXISTS: i32 = 1012; -pub const EC_IO_ERR_WOULD_BLOCK: i32 = 1013; -pub const EC_IO_ERR_NOT_A_DIRECTORY: i32 = 1014; -pub const EC_IO_ERR_IS_A_DIRECTORY: i32 = 1015; -pub const EC_IO_ERR_DIRECTORY_NOT_EMPTY: i32 = 1016; -pub const EC_IO_ERR_READ_ONLY_FILESYSTEM: i32 = 1017; -pub const EC_IO_ERR_STALE_NETWORK_FILE_HANDLE: i32 = 1018; -pub const EC_IO_ERR_INVALID_INPUT: i32 = 1019; -pub const EC_IO_ERR_INVALID_DATA: i32 = 1020; -pub const EC_IO_ERR_TIMED_OUT: i32 = 1021; -pub const EC_IO_ERR_WRITE_ZERO: i32 = 1022; -pub const EC_IO_ERR_STORAGE_FULL: i32 = 1023; -pub const EC_IO_ERR_NOT_SEEKABLE: i32 = 1024; -pub const EC_IO_ERR_QUOTA_EXCEEDED: i32 = 1025; -pub const EC_IO_ERR_FILE_TOO_LARGE: i32 = 1026; -pub const EC_IO_ERR_RESOURCE_BUSY: i32 = 1027; -pub const EC_IO_ERR_EXECUTABLE_FILE_BUSY: i32 = 1028; -pub const EC_IO_ERR_DEADLOCK: i32 = 1029; -pub const EC_IO_ERR_CROSSES_DEVICES: i32 = 1030; -pub const EC_IO_ERR_TOO_MANY_LINKS: i32 = 1031; -pub const EC_IO_ERR_INVALID_FILENAME: i32 = 1032; -pub const EC_IO_ERR_ARGUMENT_LIST_TOO_LONG: i32 = 1033; -pub const EC_IO_ERR_INTERRUPTED: i32 = 1034; -pub const EC_IO_ERR_UNSUPPORTED: i32 = 1035; -pub const EC_IO_ERR_UNEXPECTED_EOF: i32 = 1036; -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) -> RenderResult { - let mut result = RenderResult::default(); - match err.kind() { - std::io::ErrorKind::NotFound => { - writeln!(result, "{}", eformat_cargo!("file or directory not found")).ok(); - ec.exit_code = EC_IO_ERR_NOT_FOUND; - } - std::io::ErrorKind::PermissionDenied => { - writeln!(result, "{}", eformat_cargo!("permission denied")).ok(); - ec.exit_code = EC_IO_ERR_PERMISSION_DENIED; - } - std::io::ErrorKind::ConnectionRefused => { - writeln!(result, "{}", eformat_cargo!("connection refused")).ok(); - ec.exit_code = EC_IO_ERR_CONNECTION_REFUSED; - } - std::io::ErrorKind::ConnectionReset => { - writeln!(result, "{}", eformat_cargo!("connection reset")).ok(); - ec.exit_code = EC_IO_ERR_CONNECTION_RESET; - } - std::io::ErrorKind::HostUnreachable => { - writeln!(result, "{}", eformat_cargo!("host unreachable")).ok(); - ec.exit_code = EC_IO_ERR_HOST_UNREACHABLE; - } - std::io::ErrorKind::NetworkUnreachable => { - writeln!(result, "{}", eformat_cargo!("network unreachable")).ok(); - ec.exit_code = EC_IO_ERR_NETWORK_UNREACHABLE; - } - std::io::ErrorKind::ConnectionAborted => { - writeln!(result, "{}", eformat_cargo!("connection aborted")).ok(); - ec.exit_code = EC_IO_ERR_CONNECTION_ABORTED; - } - std::io::ErrorKind::NotConnected => { - writeln!(result, "{}", eformat_cargo!("not connected")).ok(); - ec.exit_code = EC_IO_ERR_NOT_CONNECTED; - } - std::io::ErrorKind::AddrInUse => { - writeln!(result, "{}", eformat_cargo!("address in use")).ok(); - ec.exit_code = EC_IO_ERR_ADDR_IN_USE; - } - std::io::ErrorKind::AddrNotAvailable => { - writeln!(result, "{}", eformat_cargo!("address not available")).ok(); - ec.exit_code = EC_IO_ERR_ADDR_NOT_AVAILABLE; - } - std::io::ErrorKind::NetworkDown => { - writeln!(result, "{}", eformat_cargo!("network down")).ok(); - ec.exit_code = EC_IO_ERR_NETWORK_DOWN; - } - std::io::ErrorKind::BrokenPipe => { - writeln!(result, "{}", eformat_cargo!("broken pipe")).ok(); - ec.exit_code = EC_IO_ERR_BROKEN_PIPE; - } - std::io::ErrorKind::AlreadyExists => { - writeln!( - result, - "{}", - eformat_cargo!("file or directory already exists") - ) - .ok(); - ec.exit_code = EC_IO_ERR_ALREADY_EXISTS; - } - std::io::ErrorKind::WouldBlock => { - writeln!(result, "{}", eformat_cargo!("operation would block")).ok(); - ec.exit_code = EC_IO_ERR_WOULD_BLOCK; - } - std::io::ErrorKind::NotADirectory => { - writeln!(result, "{}", eformat_cargo!("not a directory")).ok(); - ec.exit_code = EC_IO_ERR_NOT_A_DIRECTORY; - } - std::io::ErrorKind::IsADirectory => { - writeln!(result, "{}", eformat_cargo!("is a directory")).ok(); - ec.exit_code = EC_IO_ERR_IS_A_DIRECTORY; - } - std::io::ErrorKind::DirectoryNotEmpty => { - writeln!(result, "{}", eformat_cargo!("directory not empty")).ok(); - ec.exit_code = EC_IO_ERR_DIRECTORY_NOT_EMPTY; - } - std::io::ErrorKind::ReadOnlyFilesystem => { - writeln!(result, "{}", eformat_cargo!("read-only filesystem")).ok(); - ec.exit_code = EC_IO_ERR_READ_ONLY_FILESYSTEM; - } - std::io::ErrorKind::StaleNetworkFileHandle => { - writeln!(result, "{}", eformat_cargo!("stale network file handle")).ok(); - ec.exit_code = EC_IO_ERR_STALE_NETWORK_FILE_HANDLE; - } - std::io::ErrorKind::InvalidInput => { - writeln!(result, "{}", eformat_cargo!("invalid input")).ok(); - ec.exit_code = EC_IO_ERR_INVALID_INPUT; - } - std::io::ErrorKind::InvalidData => { - writeln!(result, "{}", eformat_cargo!("invalid data")).ok(); - ec.exit_code = EC_IO_ERR_INVALID_DATA; - } - std::io::ErrorKind::TimedOut => { - writeln!(result, "{}", eformat_cargo!("timed out")).ok(); - ec.exit_code = EC_IO_ERR_TIMED_OUT; - } - std::io::ErrorKind::WriteZero => { - writeln!(result, "{}", eformat_cargo!("write zero")).ok(); - ec.exit_code = EC_IO_ERR_WRITE_ZERO; - } - std::io::ErrorKind::StorageFull => { - writeln!(result, "{}", eformat_cargo!("storage full")).ok(); - ec.exit_code = EC_IO_ERR_STORAGE_FULL; - } - std::io::ErrorKind::NotSeekable => { - writeln!(result, "{}", eformat_cargo!("not seekable")).ok(); - ec.exit_code = EC_IO_ERR_NOT_SEEKABLE; - } - std::io::ErrorKind::QuotaExceeded => { - writeln!(result, "{}", eformat_cargo!("quota exceeded")).ok(); - ec.exit_code = EC_IO_ERR_QUOTA_EXCEEDED; - } - std::io::ErrorKind::FileTooLarge => { - writeln!(result, "{}", eformat_cargo!("file too large")).ok(); - ec.exit_code = EC_IO_ERR_FILE_TOO_LARGE; - } - std::io::ErrorKind::ResourceBusy => { - writeln!(result, "{}", eformat_cargo!("resource busy")).ok(); - ec.exit_code = EC_IO_ERR_RESOURCE_BUSY; - } - std::io::ErrorKind::ExecutableFileBusy => { - writeln!(result, "{}", eformat_cargo!("executable file busy")).ok(); - ec.exit_code = EC_IO_ERR_EXECUTABLE_FILE_BUSY; - } - std::io::ErrorKind::Deadlock => { - writeln!(result, "{}", eformat_cargo!("deadlock")).ok(); - ec.exit_code = EC_IO_ERR_DEADLOCK; - } - std::io::ErrorKind::CrossesDevices => { - writeln!(result, "{}", eformat_cargo!("crosses devices")).ok(); - ec.exit_code = EC_IO_ERR_CROSSES_DEVICES; - } - std::io::ErrorKind::TooManyLinks => { - writeln!(result, "{}", eformat_cargo!("too many links")).ok(); - ec.exit_code = EC_IO_ERR_TOO_MANY_LINKS; - } - std::io::ErrorKind::InvalidFilename => { - writeln!(result, "{}", eformat_cargo!("invalid filename")).ok(); - ec.exit_code = EC_IO_ERR_INVALID_FILENAME; - } - std::io::ErrorKind::ArgumentListTooLong => { - writeln!(result, "{}", eformat_cargo!("argument list too long")).ok(); - ec.exit_code = EC_IO_ERR_ARGUMENT_LIST_TOO_LONG; - } - std::io::ErrorKind::Interrupted => { - writeln!(result, "{}", eformat_cargo!("interrupted")).ok(); - ec.exit_code = EC_IO_ERR_INTERRUPTED; - } - std::io::ErrorKind::Unsupported => { - writeln!(result, "{}", eformat_cargo!("unsupported")).ok(); - ec.exit_code = EC_IO_ERR_UNSUPPORTED; - } - std::io::ErrorKind::UnexpectedEof => { - writeln!(result, "{}", eformat_cargo!("unexpected end of file")).ok(); - ec.exit_code = EC_IO_ERR_UNEXPECTED_EOF; - } - std::io::ErrorKind::OutOfMemory => { - writeln!(result, "{}", eformat_cargo!("out of memory")).ok(); - ec.exit_code = EC_IO_ERR_OUT_OF_MEMORY; - } - std::io::ErrorKind::Other => { - writeln!(result, "{}", eformat_cargo!(err.to_string())).ok(); - ec.exit_code = EC_IO_ERR_OTHER; - } - _ => { - writeln!(result, "{}", eformat_cargo!(err.to_string())).ok(); - ec.exit_code = EC_IO_ERR_OTHER; - } - } - result -} diff --git a/mling/src/helps/mling_help.txt b/mling/src/helps/mling_help.txt deleted file mode 100644 index 73bfd4d..0000000 --- a/mling/src/helps/mling_help.txt +++ /dev/null @@ -1,20 +0,0 @@ -Mingling's scaffolding tool - -[[b_green]]**Usage:**[[/]] [[b_cyan]]**cargo mling**[[/]] [[cyan]][COMMAND] [OPTIONS]...[[/]] - -[[b_green]]**Options:**[[/]] -__ [[b_cyan]]**-V**[[/]], [[b_cyan]]**--version**[[/]] Print version info and exit -__ [[b_cyan]]**-h**[[/]], [[b_cyan]]**--help**[[/]] Print this help message - -__ [[b_cyan]]**-P**[[/]], [[b_cyan]]**--manifest-path**[[/]] [[cyan]]<PATH>[[/]] Path to _Cargo.toml_ - -__ [[b_cyan]]**--silence**[[/]], [[b_cyan]]**--quiet**[[/]] Suppress all output -__ [[b_cyan]]**--no-error**[[/]] Suppress error output -__ [[b_cyan]]**--no-result**[[/]] Suppress result output - -[[b_green]]**Commands:**[[/]] -__ [[b_cyan]]**install**[[/]] Install current project into -__ the **mling** package manager -__ [[b_cyan]]**ls**[[/]], [[b_cyan]]**show**[[/]], [[b_cyan]]**add**[[/]], [[b_cyan]]**rm**[[/]] List, show, add, or remove something - -Run \`[[b_cyan]]**cargo help mling**[[/]]\` for more detailed information. diff --git a/mling/src/lib.rs b/mling/src/lib.rs deleted file mode 100644 index 1de1228..0000000 --- a/mling/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -#![allow(unused_imports)] - -use mingling::{ - macros::{chain, gen_program, pack, renderer}, - res::ResExitCode, -}; - -mod cargo_style; -pub use cargo_style::*; - -pub mod cli; -pub mod display; -pub mod errors; -pub mod pkg_mgr; -pub mod proj_mgr; -pub mod res; - -gen_program!(); diff --git a/mling/src/pkg_mgr/installer.rs b/mling/src/pkg_mgr/installer.rs deleted file mode 100644 index 8b13789..0000000 --- a/mling/src/pkg_mgr/installer.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/mling/src/pkg_mgr/mod.rs b/mling/src/pkg_mgr/mod.rs deleted file mode 100644 index 682b433..0000000 --- a/mling/src/pkg_mgr/mod.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::ThisProgram; -use mingling::{ - Program, - macros::{dispatcher, program_setup}, -}; - -pub mod installer; - -dispatcher!("install"); -dispatcher!("ls.namespace", CMDListNamespace => EntryListNamespace); -dispatcher!("rm.namespace", CMDRemoveNamespace => EntryRemoveNamespace); - -#[program_setup] -pub fn package_manager_setup(p: &mut Program<ThisProgram>) { - p.with_dispatcher(CMDInstall); - p.with_dispatcher(CMDListNamespace); - p.with_dispatcher(CMDRemoveNamespace); -} diff --git a/mling/src/proj_mgr/checklist_reader.rs b/mling/src/proj_mgr/checklist_reader.rs deleted file mode 100644 index 558d303..0000000 --- a/mling/src/proj_mgr/checklist_reader.rs +++ /dev/null @@ -1,294 +0,0 @@ -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). -/// -/// # Format -/// -/// - **Values**: fenced code blocks where the info string is the key and -/// the first non-empty line is the value. Example: -/// <code>\`\`\`name<br>my-cli<br>\`\`\`</code> -/// - **Toggles**: checkbox lines like `- [x] \`ns:key\`` (checked) or -/// `- [ ] \`ns:key\`` (unchecked). -/// -/// Checked items are stored as `"namespace:key"`. -/// -/// # Usage -/// -/// ```rust,ignore -/// let reader = CheckListReader::from(&Path::new("CHECKLIST.md")).unwrap(); -/// assert_eq!(reader.read_value("name"), Some("my-cli".into())); -/// assert!(reader.read_toggle("ver:0.2")); -/// assert!(!reader.read_toggle("feat:comp")); -/// assert_eq!(reader.read_toggles("feat"), vec!["feat:async"]); -/// ``` -pub struct CheckListReader { - /// Values extracted from fenced code blocks: `key → value`. - values: HashMap<String, String>, - - /// Checked toggles: `"namespace:key" → true` (only checked items are stored). - toggles: HashMap<String, bool>, - - /// All toggles (checked or not): `"namespace:key" → checked`. - all_toggles: HashMap<String, bool>, -} - -impl CheckListReader { - /// Parse a CHECKLIST.md from a file path in a single left-to-right pass - pub fn from(path: &Path) -> Result<Self, io::Error> { - let content = std::fs::read_to_string(path)?; - - let mut reader = Self { - values: HashMap::new(), - toggles: HashMap::new(), - all_toggles: HashMap::new(), - }; - reader.parse(&content); - Ok(reader) - } - - /// Parse CHECKLIST.md content in a single pass. - fn parse(&mut self, content: &str) { - let lines: Vec<&str> = content.lines().collect(); - let mut i = 0; - - while i < lines.len() { - 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(); - 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; - } - - 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); - if is_checked { - self.toggles.insert(toggle_key, true); - } - } - - i += 1; - } - } - - /// Extract the `namespace:key` from a toggle line. - /// - /// Matches: `- [x] `namespace:key`` or `- [ ] `namespace:key`` - fn parse_toggle_line(line: &str) -> Option<String> { - let line = line.trim(); - if !line.starts_with("- [") { - return None; - } - // Find the backtick-enclosed key - let start = line.find('`')?; - let rest = &line[start + 1..]; - let end = rest.find('`')?; - let key = rest[..end].trim(); - if key.is_empty() { - return None; - } - Some(key.to_string()) - } - - /// Read a value by its key. - /// - /// Returns `Some(value)` if a fenced code block with that key was found, - /// or `None` if the key does not exist. - #[must_use] - pub fn read_value(&self, key: &str) -> Option<String> { - self.values.get(key).cloned() - } - - /// Check if a toggle (`namespace:key`) is checked. - /// - /// Returns `true` if the checkbox line was `- [x]`, `false` if it was - /// `- [ ]` or the key does not exist in the file. - #[must_use] - pub fn read_toggle(&self, key: &str) -> bool { - self.toggles.contains_key(key) - } - - /// Get all toggles under a given `namespace` that are checked. - /// - /// For example, `read_toggles("feat")` returns all checked keys starting - /// with `"feat:"`, such as `["feat:comp", "feat:parser"]`. - #[must_use] - pub fn read_toggles(&self, namespace: &str) -> Vec<String> { - let prefix = format!("{namespace}:"); - let mut keys: Vec<String> = self - .toggles - .keys() - .filter(|k| k.starts_with(&prefix)) - .cloned() - .collect(); - keys.sort(); - keys - } - - /// Get ALL toggles (checked or not) under a namespace. - /// - /// Useful for iterating all available options. - #[must_use] - pub fn all_toggles(&self, namespace: &str) -> Vec<String> { - let prefix = format!("{namespace}:"); - let mut keys: Vec<String> = self - .all_toggles - .keys() - .filter(|k| k.starts_with(&prefix)) - .cloned() - .collect(); - keys.sort(); - keys - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn sample_md() -> &'static str { - r#"> Some intro - -## Question 1: What is your project name? - -```name -my-cli -``` - -## Question 2: Which version? - -- [x] `ver:0.2` - -## Question 3: Features? - -- [ ] `feat:structural_renderer` -- [x] `feat:comp` -- [x] `feat:parser` -- [ ] `feat:async` - -```other -some value -``` -"# - } - - #[test] - fn test_read_value() { - let mut reader = CheckListReader { - values: HashMap::new(), - toggles: HashMap::new(), - all_toggles: HashMap::new(), - }; - reader.parse(sample_md()); - assert_eq!(reader.read_value("name"), Some("my-cli".into())); - assert_eq!(reader.read_value("other"), Some("some value".into())); - assert_eq!(reader.read_value("nonexistent"), None); - } - - #[test] - fn test_read_toggle() { - let mut reader = CheckListReader { - values: HashMap::new(), - toggles: HashMap::new(), - all_toggles: HashMap::new(), - }; - reader.parse(sample_md()); - assert!(reader.read_toggle("ver:0.2")); - assert!(reader.read_toggle("feat:comp")); - assert!(reader.read_toggle("feat:parser")); - assert!(!reader.read_toggle("feat:structural_renderer")); - assert!(!reader.read_toggle("feat:async")); - assert!(!reader.read_toggle("nonexistent:key")); - } - - #[test] - fn test_read_toggles() { - let mut reader = CheckListReader { - values: HashMap::new(), - toggles: HashMap::new(), - all_toggles: HashMap::new(), - }; - reader.parse(sample_md()); - let feat_toggles = reader.read_toggles("feat"); - assert_eq!(feat_toggles, vec!["feat:comp", "feat:parser"]); - } - - #[test] - fn test_all_toggles() { - let mut reader = CheckListReader { - values: HashMap::new(), - toggles: HashMap::new(), - all_toggles: HashMap::new(), - }; - reader.parse(sample_md()); - let all = reader.all_toggles("feat"); - assert_eq!( - all, - vec![ - "feat:async", - "feat:comp", - "feat:parser", - "feat:structural_renderer", - ] - ); - } - - #[test] - fn test_from_path() { - // Write a temp CHECKLIST.md, read it, then clean up - let dir = std::env::temp_dir().join("mling_checklist_test"); - std::fs::create_dir_all(&dir).unwrap(); - let path = dir.join("CHECKLIST.md"); - std::fs::write(&path, sample_md()).unwrap(); - - let reader = CheckListReader::from(&path).unwrap(); - assert_eq!(reader.read_value("name"), Some("my-cli".into())); - assert!(reader.read_toggle("ver:0.2")); - - let _ = std::fs::remove_dir_all(&dir); - } - - #[test] - fn test_empty_file() { - let mut reader = CheckListReader { - values: HashMap::new(), - toggles: HashMap::new(), - all_toggles: HashMap::new(), - }; - reader.parse(""); - assert_eq!(reader.read_value("anything"), None); - assert!(!reader.read_toggle("ns:key")); - assert!(reader.read_toggles("ns").is_empty()); - } - - #[test] - fn test_no_toggle_or_value_lines() { - let md = "# Just a heading\n\nSome text\n\n```code\nstill not a value\n```\n"; - let mut reader = CheckListReader { - values: HashMap::new(), - toggles: HashMap::new(), - all_toggles: HashMap::new(), - }; - reader.parse(md); - // "code" isn't a valid value key (it's used as a language tag, not a name/value key) - // but our parser treats ANY ```key as a value block. This is correct behavior — - // malformed CHECKLIST.md may produce unexpected values. - assert_eq!(reader.read_value("code"), Some("still not a value".into())); - } -} diff --git a/mling/src/proj_mgr/generator.rs b/mling/src/proj_mgr/generator.rs deleted file mode 100644 index f0dbdd7..0000000 --- a/mling/src/proj_mgr/generator.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::path::{self, PathBuf}; - -use mingling::{ - Grouped, RenderResult, - macros::{chain, pack, renderer, route}, -}; -use std::io::Write as _; - -use crate::{Next, proj_mgr::EntryGenerateProject, res::ResCurrentDir}; - -pack!(StateGenerateProjectReady = PathBuf); -pack!(ResultGenerateProjectChecklistCreated = PathBuf); - -pack!(StateGenerateProjectExecBegin = PathBuf); -pack!(StateGenerateProjectExecuting = ()); - -const CHECK_LIST_NAME: &str = "CHECKLIST.md"; -const CHECK_LIST_CONTENT: &str = include_str!("../../res/CHECKLIST.md"); - -#[chain] -pub fn handle_generate(_args: EntryGenerateProject, cwd: &ResCurrentDir) -> Next { - let checklist_path = cwd.path.join(CHECK_LIST_NAME); - - if !checklist_path.exists() { - StateGenerateProjectReady::new(checklist_path).to_chain() - } else { - StateGenerateProjectExecBegin::new(checklist_path).to_chain() - } -} - -#[chain] -pub fn handle_state_gen_proj_ready(prev: StateGenerateProjectReady) -> Next { - let path = prev.inner; - route!(std::fs::write(&path, CHECK_LIST_CONTENT)); - ResultGenerateProjectChecklistCreated::new(path).to_render() -} - -#[renderer] -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() - ) - .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/metadata.rs b/mling/src/proj_mgr/metadata.rs deleted file mode 100644 index 1ba24e1..0000000 --- a/mling/src/proj_mgr/metadata.rs +++ /dev/null @@ -1,142 +0,0 @@ -use serde::Deserialize; -use serde::Serialize; - -use std::path::PathBuf; -use std::process::Command; - -/// Read cargo metadata by running `cargo metadata` with the given manifest path. -pub fn read_metadata(cargo_toml: &PathBuf) -> Result<CargoLockFile, std::io::Error> { - let output = Command::new("cargo") - .arg("metadata") - .arg("--format-version") - .arg("1") - .arg("--manifest-path") - .arg(cargo_toml) - .output()?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(std::io::Error::other(format!( - "cargo metadata failed: {}", - stderr - ))); - } - - let lock_file: CargoLockFile = serde_json::from_slice(&output.stdout) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; - - Ok(lock_file) -} - -/// A cargo metadata lock file that serde can serialize and deserialize. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CargoLockFile { - pub packages: Vec<Package>, - #[serde(rename = "workspace_members")] - pub workspace_members: Vec<String>, - #[serde(rename = "workspace_default_members")] - pub workspace_default_members: Vec<String>, - pub resolve: Resolve, - #[serde(rename = "target_directory")] - pub target_directory: String, - #[serde(rename = "build_directory")] - pub build_directory: String, - pub version: u64, - #[serde(rename = "workspace_root")] - pub workspace_root: String, - pub metadata: Option<serde_json::Value>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Package { - pub name: String, - pub version: String, - pub id: String, - pub license: Option<String>, - #[serde(rename = "license_file")] - pub license_file: Option<String>, - pub description: Option<String>, - pub source: Option<String>, - pub dependencies: Vec<Dependency>, - pub targets: Vec<Target>, - pub features: std::collections::BTreeMap<String, Vec<String>>, - #[serde(rename = "manifest_path")] - pub manifest_path: String, - pub metadata: Option<serde_json::Value>, - pub publish: Option<serde_json::Value>, - pub authors: Vec<String>, - pub categories: Vec<String>, - pub keywords: Vec<String>, - pub readme: Option<String>, - pub repository: Option<String>, - pub homepage: Option<String>, - pub documentation: Option<String>, - pub edition: String, - pub links: Option<String>, - #[serde(rename = "default_run")] - pub default_run: Option<String>, - #[serde(rename = "rust_version")] - pub rust_version: Option<String>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Dependency { - pub name: String, - pub source: Option<String>, - pub req: String, - pub kind: Option<String>, - pub rename: Option<String>, - pub optional: bool, - #[serde(rename = "uses_default_features")] - pub uses_default_features: bool, - pub features: Vec<String>, - pub target: Option<String>, - pub registry: Option<String>, - #[serde(skip_serializing_if = "Option::is_none")] - pub path: Option<String>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Target { - pub kind: Vec<String>, - #[serde(rename = "crate_types")] - pub crate_types: Vec<String>, - pub name: String, - #[serde(rename = "src_path")] - pub src_path: String, - pub edition: String, - pub doc: bool, - pub doctest: bool, - pub test: bool, - #[serde(skip_serializing_if = "Option::is_none")] - #[serde(rename = "required-features")] - pub required_features: Option<Vec<String>>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Resolve { - pub nodes: Vec<ResolveNode>, - pub root: Option<String>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ResolveNode { - pub id: String, - pub dependencies: Vec<String>, - pub deps: Vec<DepInfo>, - pub features: Vec<String>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DepInfo { - pub name: String, - pub pkg: String, - #[serde(rename = "dep_kinds")] - pub dep_kinds: Vec<DepKind>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DepKind { - pub kind: Option<String>, - pub target: Option<String>, -} diff --git a/mling/src/proj_mgr/mod.rs b/mling/src/proj_mgr/mod.rs deleted file mode 100644 index e0a4216..0000000 --- a/mling/src/proj_mgr/mod.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::ThisProgram; -use mingling::{ - Program, - macros::{dispatcher, program_setup}, -}; - -pub mod checklist_reader; -pub mod generator; -pub mod metadata; -pub mod show_binaries; -pub mod show_directories; - -dispatcher!("gen", CMDGenerateProject => EntryGenerateProject); - -dispatcher!("show.binaries"); -dispatcher!("show.workspace-dir", - CMDShowWorkspaceDirectory => EntryShowWorkspaceDirectory -); -dispatcher!("show.target-dir", - CMDShowTargetDirectories => EntryShowTargetDirectories -); - -#[program_setup] -pub fn project_manager_setup(p: &mut Program<ThisProgram>) { - p.with_dispatcher(CMDGenerateProject); - - p.with_dispatcher(CMDShowBinaries); - p.with_dispatcher(CMDShowWorkspaceDirectory); - p.with_dispatcher(CMDShowTargetDirectories); -} diff --git a/mling/src/proj_mgr/show_binaries.rs b/mling/src/proj_mgr/show_binaries.rs deleted file mode 100644 index 4fb5c28..0000000 --- a/mling/src/proj_mgr/show_binaries.rs +++ /dev/null @@ -1,78 +0,0 @@ -use std::path::PathBuf; - -use colored::Colorize; -use mingling::{ - Grouped, RenderResult, - macros::{chain, pack, renderer}, -}; -use serde::Serialize; -use std::io::Write as _; - -use crate::{ - Next, - proj_mgr::{ - EntryShowBinaries, - metadata::{CargoLockFile, read_metadata}, - }, - res::ResManifestPath, -}; - -#[derive(Serialize, Grouped)] -pub struct ResultBinaries { - pub binaries: Vec<DataBinary>, -} - -#[derive(Serialize)] -pub struct DataBinary { - pub name: String, - pub path: PathBuf, -} - -#[chain] -pub fn handle_show_binaries(_args: EntryShowBinaries, manifest_path: &ResManifestPath) -> Next { - let metadata = read_metadata(manifest_path.resolved()).unwrap(); - let CargoLockFile { - packages, - workspace_members, - .. - } = metadata; - - let binaries: Vec<DataBinary> = packages - .into_iter() - .filter(|pkg| workspace_members.contains(&pkg.id)) - .flat_map(|pkg| { - pkg.targets - .into_iter() - .filter(|target| target.kind.iter().any(|k| k == "bin")) - .map(move |target| DataBinary { - name: target.name, - path: PathBuf::from(pkg.manifest_path.clone()) - .parent() - .unwrap_or(&PathBuf::from(".")) - .join("src") - .join(&target.src_path), - }) - }) - .collect(); - - ResultBinaries { binaries }.to_render() -} - -#[renderer] -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 { - 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 deleted file mode 100644 index 5c5f448..0000000 --- a/mling/src/proj_mgr/show_directories.rs +++ /dev/null @@ -1,61 +0,0 @@ -use colored::Colorize; -use mingling::{ - Grouped, RenderResult, - macros::{chain, pack, renderer}, -}; -use serde::Serialize; -use std::io::Write as _; - -use crate::{ - Next, - proj_mgr::{EntryShowTargetDirectories, EntryShowWorkspaceDirectory, metadata::read_metadata}, - res::ResManifestPath, -}; - -#[derive(Serialize, Grouped)] -pub struct ResultWorkspaceDirectory { - pub path: String, -} - -#[derive(Serialize, Grouped)] -pub struct ResultTargetDirectory { - pub path: String, -} - -#[chain] -pub fn handle_show_workspace_directory( - _args: EntryShowWorkspaceDirectory, - manifest_path: &ResManifestPath, -) -> Next { - let metadata = read_metadata(manifest_path.resolved()).unwrap(); - ResultWorkspaceDirectory { - path: metadata.workspace_root, - } - .to_render() -} - -#[chain] -pub fn handle_show_target_directory( - _args: EntryShowTargetDirectories, - manifest_path: &ResManifestPath, -) -> Next { - let metadata = read_metadata(manifest_path.resolved()).unwrap(); - ResultTargetDirectory { - path: metadata.target_directory, - } - .to_render() -} - -#[renderer] -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) -> RenderResult { - let mut result = RenderResult::default(); - writeln!(result, "{}", prev.path.bright_cyan().bold()).ok(); - result -} diff --git a/mling/src/res/current_dir.rs b/mling/src/res/current_dir.rs deleted file mode 100644 index b928596..0000000 --- a/mling/src/res/current_dir.rs +++ /dev/null @@ -1,6 +0,0 @@ -use std::path::PathBuf; - -#[derive(Default, Clone)] -pub struct ResCurrentDir { - pub path: PathBuf, -} diff --git a/mling/src/res/manifest_path.rs b/mling/src/res/manifest_path.rs deleted file mode 100644 index 7f1686e..0000000 --- a/mling/src/res/manifest_path.rs +++ /dev/null @@ -1,13 +0,0 @@ -use std::path::PathBuf; - -#[derive(Default, Clone)] -pub struct ResManifestPath { - pub raw: Option<String>, - pub resolved: Option<PathBuf>, -} - -impl ResManifestPath { - pub fn resolved(&self) -> &PathBuf { - self.resolved.as_ref().unwrap() - } -} diff --git a/mling/src/res/mod.rs b/mling/src/res/mod.rs deleted file mode 100644 index caa843c..0000000 --- a/mling/src/res/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod current_dir; -pub use current_dir::*; - -mod manifest_path; -pub use manifest_path::*; |
