diff options
172 files changed, 6089 insertions, 3388 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..71ced58 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,18 +1,15 @@ { - "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.files.exclude": ["**/target/**", "**/.temp/**"], + "rust-analyzer.linkedProjects": [ + ".run/Cargo.toml", + "mingling_pathf/test/Cargo.toml", + "arg_picker/Cargo.toml", + "arg_picker/test/Cargo.toml", + "mingling_cli/Cargo.toml" + ], + "rust-analyzer.cargo.features": [], + "rust-analyzer.procMacro.enable": true, + "rust-analyzer.procMacro.attributes.enable": true } diff --git a/.zed/settings.json b/.zed/settings.json index 727f955..7378109 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -1,26 +1,29 @@ { - "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" }, + "files": { + "exclude": ["**/target/**", "**/.temp/**"] + }, + "linkedProjects": [ + ".run/Cargo.toml", + "mingling_pathf/test/Cargo.toml", + "arg_picker/Cargo.toml", + "arg_picker/test/Cargo.toml", + "mingling_cli/Cargo.toml" + ], + "cargo": { + "features": [] + }, + "procMacro": { + "enable": true, + "attributes": { + "enable": true + } + } + } + } + } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 54cffcf..86584cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,9 +52,34 @@ None ### Release 0.3.0 (Unreleased) +> In detail, the changes in Mingling 0.3.0 are as follows: + +1. **Added `arg-picker`** — Mingling has never had a comfortable argument parsing solution. You either suffered with `parser` or went all-in on `clap`. So I wrote a smarter `arg-picker`. The API style is close to the original `parser`, but it's more type-safe, more robust, and more extensible. See the main text for details. + +2. **Made implicit behavior explicit**. For a long time, Mingling's attribute macros have been making **implicit** modifications to the original function — I have to admit, that's dirty. In the new version, I've removed **all** implicit modifications to the original function by attribute macros. In other words, `#[chain]`, `#[renderer]`, `#[help]`, and `#[completion]` will no longer modify your original function in any way unless you explicitly specify it. Use the Extension Attribute (`#[chain(/* ... */)]`) mechanism to explicitly inject implicit behavior into your functions. + +3. I was originally planning to remove `r_println!` because I couldn't stand that `__renderer_inner_result` thing implicitly injected by the `#[renderer]` macro. But now I've rewritten it: `#[buffer]` injects an implicit `__render_result_buffer` value into the function, and then `r_println!` calls it. It's an extra step, but it also means: the dirt is your choice, not something I'm forcing on you :) + +4. **Finally**, a philosophical point. Mingling will move forward with a preference for **"selectively dirty"** over **"invisibly, forcibly dirty"** — this is the biggest direction going forward, building a more comfortable API on this foundation. + #### Fixes: -None +1. **[`pathf:patterns`]** Fixed pattern detection logic in `mingling_pathf` for multiple patterns to correctly detect opening bracket forms (e.g., `[chain`, `[renderer`, `[help`, `[completion`) in addition to the previously supported closing bracket forms (e.g., `chain]`, `renderer]`, `help]`, `completion]`). This ensures that attribute macro usages like `#[chain]`, `#[renderer]`, `#[help]`, and `#[completion]` are properly detected regardless of which side of the attribute the pattern matcher examines. + + - **`ChainPattern`**: Changed `content.contains("chain]")` → `content.contains("[chain") || content.contains("chain]")` + - **`RendererPattern`**: Changed `content.contains("renderer]")` → `content.contains("[renderer") || content.contains("renderer]")` + - **`HelpPattern`**: Changed `content.contains("help]")` → `content.contains("[help") || content.contains("help]")` + - **`CompletionPattern`**: Changed `content.contains("completion(")` → `content.contains("completion(") || content.contains("[completion")` + +2. **[`pathf:patterns`]** Updated `PackPattern` detection to recognize `pack_structural!` and `pack_err_structural!` macros, which were previously missed by the pattern matcher. The `contains` method now checks for these additional macro names alongside the existing `pack!` and `pack_err!` checks. + +3. **[`pathf:patterns`]** Added `is_foreign` field to `AnalyzeItem` struct in `mingling_pathf`, along with constructor helpers `AnalyzeItem::local()` and `AnalyzeItem::foreign()`. The `foreign()` constructor marks items resolved via `use` imports, so their `module` path is used as-is (rather than being prefixed with the file's module path) in `type_mapping_builder.rs`. + + - **`GroupPattern`** updated to collect `use` imports at the file and inline-module level via `collect_use_imports()` and `collect_from_use_tree()`, and to resolve `group!(TypeName)` invocations against those imports: if a name matches an imported type, it is emitted as `AnalyzeItem::foreign()` with the full import path; if the `Alias = path::Type` form is used, it is emitted as `AnalyzeItem::local()` (the alias lives in-crate); otherwise it is `local()`. + + - All other patterns (`BasicStructPattern`, `ChainPattern`, `CompletionPattern`, `DispatcherPattern`, `DispatcherClapPattern`, `GroupedDerivePattern`, `HelpPattern`, `PackPattern`, `RendererPattern`) updated to use `AnalyzeItem::local()` constructors, preserving existing behavior (all items are treated as local/in-crate). + + - **`type_mapping_builder.rs`** updated to check `ai.is_foreign`: when true, the full path is built as `{module}::{item_name}` (no prefix from the file's own module path); when false, the existing logic applies (`{file_module_path}::{module}::{item_name}` or `{file_module_path}::{item_name}`). #### Optimizations: @@ -79,6 +104,66 @@ None _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._ + +4. **[`core`]** Changed the `exec_without_render` and `exec` methods' behavior: the `print!`-based output with manual stdout flushing has been replaced by a call to `result.std_print()`, which handles buffered output internally via the new `RenderResult` buffer system (introduced in **BREAKING CHANGE #6**). The `render_output` gating still applies — when `stdout_setting.render_output` is `false`, nothing is printed — but the `!result.is_empty()` guard has been removed: `std_print()` now handles empty results appropriately. + + Previously, when `render_output` was true but `result.is_empty()` was also true, the old code would skip printing entirely and return the exit code from the result. Now, `std_print()` is called unconditionally when `render_output` is true, and the exit code is read from the result regardless of whether any output was printed. This ensures that programs which set a non-zero exit code without producing renderable output (e.g., via `ExitCodeSetup` or `ProgramControlUnit::OverrideExitCode`) will exit with the correct code. + +5. **[`core`]** Made `with_resource`, `with_dispatcher`, `with_dispatchers`, `with_hook`, and `with_setup` methods return `&mut Self` instead of `()`. These methods now return a mutable reference to the program instance, enabling ergonomic chaining: + + ```rust + // Before — required separate statements + program.with_resource(ResConfig::new()); + program.with_resource(ResDatabase::new()); + program.with_setup(BasicProgramSetup); + program.with_hook(logging_hook); + + // After — supports method chaining + program + .with_resource(ResConfig::new()) + .with_resource(ResDatabase::new()) + .with_setup(BasicProgramSetup) + .with_hook(logging_hook); + ``` + + Affected methods: + - `Program::with_resource(&mut self, res: Res) -> &mut Self` — Insert a resource into the program's global resource store. + - `Program::with_dispatcher(&mut self, dispatcher: Disp) -> &mut Self` — Add a single dispatcher to the program. + - `Program::with_dispatchers(&mut self, dispatchers: D) -> &mut Self` — Add multiple dispatchers to the program. + - `Program::with_hook(&mut self, hook: ProgramHook<C>) -> &mut Self` — Add a lifecycle hook to the program. + - `Program::with_setup(&mut self, setup: S) -> &mut Self` — Load and execute a program setup. + + _No behavioral changes — all existing functionality is preserved. Downstream code that ignores the return value continues to work without modification._ + +6. **[`core`]** **`StructuralData` trait now takes a generic parameter `C`.** The `StructuralData` trait and its sealed supertrait `StructuralDataSealed` (both under `::mingling::__private`) have been made generic over a program collector type `C: ProgramCollect<Enum = C>`. This change is necessary for `group_structural!` to bypass the orphan rule — by tying `StructuralData<C>` to `crate::ThisProgram` (which is defined in the user's crate), external types can implement `StructuralData<crate::ThisProgram>` without violating coherence. + + **Migration guide (only relevant for manual `StructuralData` implementations):** + + - All `impl StructuralData for MyType` must be updated to `impl StructuralData<crate::ThisProgram> for MyType`. + - All `impl StructuralDataSealed for MyType` must be updated to `impl StructuralDataSealed<crate::ThisProgram> for MyType`. + - All trait bounds `T: StructuralData` must be updated to `T: StructuralData<C>` with an additional `C: ProgramCollect<Enum = C>` bound. + - The `StructuralRenderer::render` method signature has changed from `render<T: StructuralData + Send>(...)` to `render<T, C>(...) where T: StructuralData<C> + Send, C: ProgramCollect<Enum = C>`. + + **Internal changes:** + + - `StructuralDataSealed` in `mingling_core::__private` now takes a `C` type parameter with `C: ProgramCollect<Enum = C>`. + - `StructuralData` in `mingling_core::renderer::structural::structural_data` now takes a `C` type parameter with `C: ProgramCollect<Enum = C>`. + - Both traits remain under `::mingling::__private`, so this change does **not** affect the public API surface. + - `StructuralRenderer::render` now takes an additional generic parameter `C: ProgramCollect<Enum = C>`. + - All derive macro and `pack_structural!` / `pack_err_structural!` / `group_structural!` implementations have been updated to emit `impl StructuralDataSealed<crate::ThisProgram>` and `impl StructuralData<crate::ThisProgram>` instead of the non-generic form. + - Test code has been updated to use `MockProgramCollect` where appropriate, and integration tests now use `crate::ThisProgram` and call `gen_program!()`. + + _No behavioral changes — this is purely a type-system refactoring to enable `group_structural!` to work with external types. Since both traits are defined in `::mingling::__private`, this change has **no impact on the public API** — end users interact with `StructuralData` only through auto-generated derive macros and `pack_structural!`/`group_structural!` macros, which are automatically updated. Only users with manual `impl StructuralData` blocks (an advanced/rare case) need to update their code. + #### 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!`. @@ -262,24 +347,117 @@ None - 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`. +12. **[`core`]** **[`macros`]** Added the `r_append!` macro and `RenderResult::append_other()` method for appending the contents of one `RenderResult` to another. The `append_other()` method on `RenderResult` merges the buffered content (text and output modes) from another `RenderResult` into the current one. If the destination result has `immediate_output` enabled but the source does not, the source's content will be immediately flushed to the appropriate output stream (stdout/stderr) while also being appended to the render buffer. The `exit_code` of the source result is **not** transferred — only the buffered content and the `immediate_output` flag are merged. + + The `r_append!` macro supports two usage forms: + - **Explicit buffer** — `r_append!(dst, src)` appends the contents of `src` into the `dst` `RenderResult`. + - **Implicit buffer** — `r_append!(src)` appends the contents of `src` into the implicit `__render_result_buffer` (available inside `#[buffer]` functions). + + The macro is re-exported as `mingling::macros::r_append` and included in `mingling::prelude::*`. + +13. **[`core`]** **[`macros`]** Added `From<F>` implementation for `RenderResult` where `F: FnOnce() -> RenderResult`. This enables `RenderResult` to be constructed from a closure or function pointer that returns a `RenderResult`, enabling ergonomic composition of render buffers. + + This is particularly useful with the `#[buffer]` extension attribute, which creates a separate buffer function that can be called from within a `#[renderer(buffer)]` function using `r_append!`: + + ```rust + use mingling::macros::{buffer, r_append, r_println}; + + // A standalone buffer function + #[buffer] + fn print_sth() { + r_println!("ok"); + } + + #[renderer(buffer)] + fn render_greet(_: ResultGreet) { + r_append!(print_sth); // appends `a`'s RenderResult content into `p`'s buffer + } + ``` + + Under the hood, `r_append!` (when used in implicit buffer mode inside a `#[buffer]` function) calls `append_other` on the current render buffer. The `From<F>` implementation allows the buffer function's return value (a `RenderResult`) to be seamlessly converted into the parent's buffer via `append_other`. This enables clean separation of render logic into reusable buffer functions that can be composed together. + +14. **[`core`]** **[`macros`]** Added the `render_route!` macro and `#[renderify]` extension attribute macro, providing error routing to the rendering pipeline (as opposed to `route!`/`#[routeify]` which route to the chain pipeline). + + The `render_route!` macro is conceptually similar to `route!`, but instead of routing errors through `Routable::to_chain()` (returning `ChainProcess`), it routes them directly to the renderer via `crate::ThisProgram::render(AnyOutput::new(e))` (returning `RenderResult`). This makes it suitable for use in `#[renderer]` and `#[help]` functions where the return type is `RenderResult`. + + ```rust,ignore + use mingling::macros::{renderer, render_route}; + + #[renderer] + fn render_something(prev: SomeType) -> RenderResult { + let data = render_route!(fetch_data().map_err(|e| ErrorEntry::new(e.to_string())))?; + // ... render data + Ok(RenderResult::new()) + } + ``` + + The `#[renderify]` extension attribute is the rendering-pipeline counterpart to `#[routeify]`. It transforms `expr?` into `render_route!(expr)` (instead of `route!(expr)`), enabling concise error routing in renderer and help functions using the `?` operator syntax. + + ```rust,ignore + #[renderer(renderify)] + fn render_greeting(prev: Greeting) -> RenderResult { + let data = load_data()?; // expands to render_route!(load_data()) + r_println!("{data}"); + Ok(RenderResult::new()) + } + ``` + + The `#[renderify]` macro can be used: + - **Standalone** — as a direct attribute: `#[renderify] fn render(...) { ... }` + - **As an extension** — via the extension point system: `#[renderer(renderify)] fn render(...) { ... }` or `#[help(renderify)] fn help(...) { ... }` + + When used as a renderer/help extension, the `renderify` identifier is detected by the extension point mechanism, stripped from the attribute arguments, and `#[renderify]` is applied as an outer attribute on top of `#[renderer]`/`#[help]` — just like `routeify` works with `#[chain]`. + + Both `render_route!` and `#[renderify]` are feature-gated behind `extra_macros` and re-exported as `mingling::macros::render_route` and `mingling::macros::renderify` respectively. + + Internal changes: + - Added `mingling_macros/src/extensions/renderify.rs` with `renderify_impl` implementation. + - Registered `#[proc_macro] pub fn render_route` and `#[proc_macro_attribute] pub fn renderify` in `mingling_macros/src/lib.rs`. + +15. **[`macros`]** Added the `#[mlint(...)]` marker attribute macro — a no-op attribute that passes its attached item through unchanged. The attribute content is ignored by `rustc` and reserved for the Mingling lint (`mlint`) tooling system. + + The `#[mlint]` attribute is registered as a `#[proc_macro_attribute]` and re-exported as `mingling::macros::mlint`. It supports three styles of lint configuration: + + ```rust,ignore + #[mlint(allow(MLINT_SOME_LINT))] + #[mlint(warn(MLINT_SOME_LINT))] + #[mlint(deny(MLINT_SOME_LINT))] + fn some_item() {} + ``` + + Since the attribute is a no-op at compile time, it has no effect on code generation, type checking, or runtime behavior. Its purpose is to serve as a structured annotation that `mlint` tooling can parse from the AST. The attribute is feature-gated behind `extra_macros`. + #### **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`). @@ -325,6 +503,104 @@ 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._ + +6. **[`core`]** **[BREAKING]** Removed the `Deref<Target = str>` implementation from `RenderResult`. Previously, `RenderResult` implemented `Deref<Target = str>`, delegating to the internal `render_text: String` field. This implementation has been removed as part of the internal refactoring of `RenderResult` from a single `String` field to a `Vec<(String, RenderResultMode)>` buffer. + + The `RenderResult` struct has been restructured internally: + + - **Removed:** `render_text: String` — replaced by a buffered storage format + - **Added:** `render_buffer: Vec<(String, RenderResultMode)>` — a list of (text, output-mode) pairs + - **Added:** `immediate_output: bool` — flag for real-time flushing + + **New `RenderResultMode` enum** (`Stdout` / `Stderr`) has been added to distinguish output streams at the buffer level. Both variants are re-exported as `mingling::RenderResultMode`. + + **New methods added to `RenderResult`:** + + - `append_to_buffer(text, mode)` / `append_line_to_buffer(text, mode)` — Append text with an explicit output mode (`Stdout` or `Stderr`) + - `eprint(text)` / `eprintln(text)` — Append text marked for stderr output + - `immediate_output()` — Enable real-time output flushing + - `std_print()` — Flush all buffered content to stdout/stderr + - `len()` / `is_empty()` — Character count and emptiness check (based on the buffer) + - `trim_buffer(self) -> RenderResult` — Trim whitespace from the buffer ends, returning a new `RenderResult` + + **`r_eprint!` and `r_eprintln!` macros** have been added to `mingling_macros` and re-exported via `mingling::macros::r_eprint` / `mingling::macros::r_eprintln` and `mingling::prelude::*`. These work analogously to `r_print!` / `r_println!` but target stderr output (calling `RenderResult::eprint` / `RenderResult::eprintln` under the hood). Both support implicit buffer mode (inside `#[buffer]` functions) and explicit buffer mode (passing a `RenderResult` as the first argument). + + The `Display` implementation for `RenderResult` is now: `write!(f, "{}", render_result_to_string(self).trim())` — this trims leading and trailing whitespace from the rendered output, making formatting more predictable and avoiding stray newlines or spaces in display contexts. + + **Migration guide:** + + Code that relied on `Deref<Target = str>` (e.g., `&*result`, `result.as_ref()`, or passing `&RenderResult` where `&str` was expected) must be updated to use one of the following: + + ```rust + // Before — relied on Deref + fn takes_str(s: &str) { /* ... */ } + takes_str(&result); + + // After — convert explicitly + let result_string: String = result.to_string(); + takes_str(&result_string); + + // Or use the Display impl directly + println!("{}", result); + ``` + + The `is_empty()` method now checks the buffer length (in characters) rather than checking `render_text.is_empty()`. The `Display` implementation no longer adds a trailing newline (previously `writeln!` was used; now `write!` is used) — existing code that relied on the trailing newline in `Display` may need adjustment. Additionally, the `to_string()` call on `RenderResult` now trims leading and trailing whitespace from the rendered text via the `Display` implementation, whereas previously the raw content was preserved without trimming. + + All examples and internal usages have been updated across the codebase to reflect these changes (e.g., `repl_basic_setup` now calls `println!("{}", r.result)` instead of `println!("{}", r.result.trim())`, since `Display` no longer adds a trailing newline). + +7. **[`any`]** **[`macros`]** Made `AnyOutput`'s `type_id` and `member_id` fields private (`pub(crate)`) and added public accessor methods `type_id()` and `member_id()`. Added the `unsafe fn new_bare<T>(value: T, member_id: G) -> Self` constructor that bypasses the `Grouped` trait, allowing manual specification of `member_id` without requiring the concrete type to implement `Grouped`. + + - **`type_id`** field changed from `pub` to `pub(crate)` — accessible via `type_id()` accessor. + - **`member_id`** field changed from `pub` to `pub(crate)` — accessible via `member_id()` accessor (requires `G: Copy`). + - **`new_bare`** — Unsafe constructor that takes a raw `member_id` value without invoking `Grouped::member_id()`. The caller must ensure the provided `member_id` correctly corresponds to the concrete type `T`. + - Updated all internal `match any.member_id { ... }` patterns in `gen_program.rs` to use `match any.member_id() { ... }` instead. + - Updated the panic message in `do_chain` (both sync and async) from `any.type_id` to `any.type_id()`. + - Updated the example-hook `main.rs` to call `info.output.member_id()` instead of accessing `info.output.member_id` directly. + - Added `Copy` derive to the generated enum to enable `member_id()`'s `Copy` requirement on the enum type. + + _No behavioral changes for existing code — the accessor methods provide the same values as the previously-public fields._ + +8. **[`any`]** **[`macros`]** **[BREAKING]** Marked `Grouped` trait as `unsafe trait`. The `Grouped` trait has always been inherently unsafe — the `member_id()` return value must exactly correspond to the variant registered by `register_type!` for the concrete type, otherwise dispatching on that type will result in **undefined behavior**. This unsoundness has existed since the trait's inception but was previously unenforced at the type system level. + + By making `Grouped` an `unsafe trait`, implementors must now explicitly acknowledge this safety contract with `unsafe impl Grouped<...> for ...`. This change makes the existing safety invariant visible to developers and enables soundness warnings at compile time. + + **Changes made:** + + - **`Grouped` trait** in `mingling_core/src/any/group.rs` changed from `pub trait Grouped<Group>` to `pub unsafe trait Grouped<Group>`, with a safety doc comment explaining that manually implementing the trait with an incorrect `member_id` leads to undefined behavior. + + - **Derive macros** (`#[derive(Grouped)]`, `#[derive(GroupedSerialize)]`) now generate `unsafe impl` instead of `impl`, with a SAFETY comment stating that the derive macro guarantees correctness because the `Ident` used in `register_type!` matches the `Ident` returned by `member_id()`. + + - **`pack!`, `pack_structural!`, `group!`, `group_structural!`** macros now generate `unsafe impl` instead of `impl`, with analogous SAFETY comments. + + - **All manual test implementations** of `Grouped` across the codebase (in `any.rs` tests, `hook.rs` tests, `mock.rs`) updated to `unsafe impl` with SAFETY comments explaining why they are safe in their test contexts. + + - **`MockProgramCollect::member_id()`** changed from `MockProgramCollect::Foo` to `panic!("Attempting to read an unsafe enum type")` to prevent accidental execution in production paths. + + **Migration guide:** + + - Existing code that uses `Grouped` only through the derive macro or `pack!`/`group!` macros is automatically migrated — no changes needed. + - Code with **manual** `impl Grouped<...> for ...` blocks must add `unsafe` before `impl` and verify that the `member_id()` return value correctly corresponds to the type's registered variant. Only proceed if the correspondence is guaranteed. + + _This is a breaking change only for code with manual `Grouped` implementations._ + --- ## 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..621471e 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!() } @@ -597,7 +551,7 @@ fn main() { .on_pre_chain(|info| { println!("[DEBUG] Pre chain: {}", info.input); }) - .on_post_chain(|info| println!("[DEBUG] Post chain: {}", info.output.member_id)) + .on_post_chain(|info| println!("[DEBUG] Post chain: {}", info.output.member_id())) .on_finish(|_| { println!("[DEBUG] Loop end"); ProgramControlUnit::OverrideExitCode(0) // Override exit code @@ -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/13-hook.md b/docs/_zh_CN/pages/13-hook.md index 6d6018a..ad3008a 100644 --- a/docs/_zh_CN/pages/13-hook.md +++ b/docs/_zh_CN/pages/13-hook.md @@ -71,7 +71,7 @@ fn main() { eprintln!("[hook] executing chain for: {}", info.input); }) .on_post_chain(|info| { - eprintln!("[hook] chain output: {}", info.output.member_id); + eprintln!("[hook] chain output: {}", info.output.member_id()); }), ); 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/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/13-hook.md b/docs/pages/13-hook.md index 26f8712..d927a9c 100644 --- a/docs/pages/13-hook.md +++ b/docs/pages/13-hook.md @@ -71,7 +71,7 @@ fn main() { eprintln!("[hook] executing chain for: {}", info.input); }) .on_post_chain(|info| { - eprintln!("[hook] chain output: {}", info.output.member_id); + eprintln!("[hook] chain output: {}", info.output.member_id()); }), ); 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-hook/src/main.rs b/examples/example-hook/src/main.rs index 23b87c7..da92045 100644 --- a/examples/example-hook/src/main.rs +++ b/examples/example-hook/src/main.rs @@ -40,7 +40,7 @@ fn main() { .on_pre_chain(|info| { println!("[DEBUG] Pre chain: {}", info.input); }) - .on_post_chain(|info| println!("[DEBUG] Post chain: {}", info.output.member_id)) + .on_post_chain(|info| println!("[DEBUG] Post chain: {}", info.output.member_id())) .on_finish(|_| { println!("[DEBUG] Loop end"); ProgramControlUnit::OverrideExitCode(0) // Override exit code diff --git a/examples/example-unit-test/src/main.rs b/examples/example-unit-test/src/main.rs index b85ff01..e9169df 100644 --- a/examples/example-unit-test/src/main.rs +++ b/examples/example-unit-test/src/main.rs @@ -42,25 +42,25 @@ mod tests { #[test] fn test_render_result_name() { let r = render_result_name(ResultName::new("Peter".into())); - assert_eq!(r.to_string().as_str(), "Hello, Peter!\n") + assert_eq!(r.to_string().as_str(), "Hello, Peter!") } #[test] fn test_render_error_no_name_provided() { let r = render_error_no_name_provided(ErrorNoNameProvided::default()); - assert_eq!(r.to_string().as_str(), "No name provided\n") + assert_eq!(r.to_string().as_str(), "No name provided") } #[test] fn test_render_error_name_not_available() { let r = render_error_name_not_available(ErrorNameNotAvailable::default()); - assert_eq!(r.to_string().as_str(), "Name not available\n") + assert_eq!(r.to_string().as_str(), "Name not available") } #[test] fn test_render_error_name_too_long() { let r = render_error_name_too_long(ErrorNameTooLong::new(17)); - assert_eq!(r.to_string().as_str(), "Name too long: 17 > 10\n") + assert_eq!(r.to_string().as_str(), "Name too long: 17 > 10") } // --------- IMPORTANT --------- } diff --git a/mingling/Cargo.toml b/mingling/Cargo.toml index 2509029..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", @@ -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 bdefb5a..89d5af1 100644 --- a/mingling/src/example_docs.rs +++ b/mingling/src/example_docs.rs @@ -1649,7 +1649,7 @@ pub mod example_help {} /// .on_pre_chain(|info| { /// println!("[DEBUG] Pre chain: {}", info.input); /// }) -/// .on_post_chain(|info| println!("[DEBUG] Post chain: {}", info.output.member_id)) +/// .on_post_chain(|info| println!("[DEBUG] Post chain: {}", info.output.member_id())) /// .on_finish(|_| { /// println!("[DEBUG] Loop end"); /// ProgramControlUnit::OverrideExitCode(0) // Override exit code @@ -2796,25 +2796,25 @@ pub mod example_structural_renderer {} /// #[test] /// fn test_render_result_name() { /// let r = render_result_name(ResultName::new("Peter".into())); -/// assert_eq!(r.to_string().as_str(), "Hello, Peter!\n") +/// assert_eq!(r.to_string().as_str(), "Hello, Peter!") /// } /// /// #[test] /// fn test_render_error_no_name_provided() { /// let r = render_error_no_name_provided(ErrorNoNameProvided::default()); -/// assert_eq!(r.to_string().as_str(), "No name provided\n") +/// assert_eq!(r.to_string().as_str(), "No name provided") /// } /// /// #[test] /// fn test_render_error_name_not_available() { /// let r = render_error_name_not_available(ErrorNameNotAvailable::default()); -/// assert_eq!(r.to_string().as_str(), "Name not available\n") +/// assert_eq!(r.to_string().as_str(), "Name not available") /// } /// /// #[test] /// fn test_render_error_name_too_long() { /// let r = render_error_name_too_long(ErrorNameTooLong::new(17)); -/// assert_eq!(r.to_string().as_str(), "Name too long: 17 > 10\n") +/// assert_eq!(r.to_string().as_str(), "Name too long: 17 > 10") /// } /// // --------- IMPORTANT --------- /// } 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 5cd53db..fd274aa 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 @@ -68,6 +75,9 @@ pub mod macros { pub use mingling_macros::group_structural; /// `#[help]` - Used to generate a struct implementing the `HelpRequest` trait via a method pub use mingling_macros::help; + /// `#[mlint(...)]` - Marker attribute for the Mingling lint system. + /// Content is ignored by rustc and reserved for mlint tooling. + pub use mingling_macros::mlint; /// `node!("remote.rm")` - Used to create a `Node` struct via a literal pub use mingling_macros::node; /// `pack!(StateGreet = String)` - Used to create a wrapper type for use with `Chain` and `Renderer` @@ -91,6 +101,21 @@ pub mod macros { /// `#[program_setup]` - Used to generate program setup #[cfg(feature = "extra_macros")] pub use mingling_macros::program_setup; + /// `r_append!` - Appends the contents of one `RenderResult` to another. + /// See the macro documentation for implicit vs. explicit buffer usage. + pub use mingling_macros::r_append; + /// `r_eprint!` - Prints text to a `RenderResult` error buffer (without newline). + /// See the macro documentation for implicit vs. explicit buffer usage. + pub use mingling_macros::r_eprint; + /// `r_eprintln!` - Prints text to a `RenderResult` error buffer (with newline). + /// See the macro documentation for implicit vs. explicit buffer usage. + pub use mingling_macros::r_eprintln; + /// `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)] @@ -101,8 +126,17 @@ pub mod macros { pub use mingling_macros::register_renderer; #[doc(hidden)] pub use mingling_macros::register_type; + /// `render_route! { /* ... */ }` - Routes errors to the rendering pipeline. + /// Similar to `route!`, but used in `#[renderer]` and `#[help]` functions + /// where the return type is `RenderResult` instead of `ChainProcess`. + #[cfg(feature = "extra_macros")] + pub use mingling_macros::render_route; /// `#[renderer]` - Used to generate a struct implementing the `Renderer` trait via a method pub use mingling_macros::renderer; + /// `#[renderify]` - An extension attribute macro that transforms `expr?` into `render_route!(expr)`. + /// Can be used standalone or as a renderer/help extension: `#[renderer(renderify, ...)]`, `#[help(renderify, ...)]`. + #[cfg(feature = "extra_macros")] + pub use mingling_macros::renderify; /// `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; @@ -131,8 +165,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::*; } @@ -175,12 +210,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; @@ -212,6 +250,21 @@ 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_append!` - Appends the contents of one `RenderResult` to another. + /// See the macro documentation for implicit vs. explicit buffer usage. + pub use mingling_macros::r_append; + /// `r_eprint!` - Prints text to a `RenderResult` error buffer (without newline). + /// See the macro documentation for implicit vs. explicit buffer usage. + pub use mingling_macros::r_eprint; + /// `r_eprintln!` - Prints text to a `RenderResult` error buffer (with newline). + /// See the macro documentation for implicit vs. explicit buffer usage. + pub use mingling_macros::r_eprintln; + /// `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"))] @@ -226,8 +279,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..150048b 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), } @@ -73,7 +75,7 @@ where fn setup(self, program: &mut Program<C>) { program.with_hook(ProgramHook::empty().on_repl_receive_result(|r| { if !r.result.is_empty() { - println!("{}", r.result.trim()) + println!("{}", r.result) } })); } diff --git a/mingling_cli/.gitignore b/mingling_cli/.gitignore new file mode 100644 index 0000000..6a6d575 --- /dev/null +++ b/mingling_cli/.gitignore @@ -0,0 +1 @@ +registry.json diff --git a/mingling_cli/Cargo.lock b/mingling_cli/Cargo.lock new file mode 100644 index 0000000..e617d0a --- /dev/null +++ b/mingling_cli/Cargo.lock @@ -0,0 +1,556 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "annotate-snippets" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" +dependencies = [ + "anstyle", + "memchr", + "unicode-width", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "arg-picker" +version = "0.1.0" +dependencies = [ + "arg-picker-macros", + "just_fmt 0.2.0", +] + +[[package]] +name = "arg-picker-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "camino" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "cargo_metadata" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" +dependencies = [ + "camino", + "cargo-platform", + "derive_builder", + "semver", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.119", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "just_fmt" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" + +[[package]] +name = "just_fmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" + +[[package]] +name = "just_template" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb99a3c1dee7299c57b26ef927f15535da0fbc93d6deb1d1114cae1337be4fb" +dependencies = [ + "just_fmt 0.1.2", + "just_template_macros", +] + +[[package]] +name = "just_template_macros" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1471eb68722ecefeb71debdde2859e8725341f171d3f42b3a98a0862ad19416e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "libc" +version = "0.2.187" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7743783ea728ef5c31194c6590797eed286449b4a4e87d626d8a51f0a94e732" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mingling" +version = "0.3.0" +dependencies = [ + "arg-picker", + "mingling_core", + "mingling_macros", +] + +[[package]] +name = "mingling-cli" +version = "0.3.0" +dependencies = [ + "annotate-snippets", + "cargo_metadata", + "just_template", + "mingling", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 3.0.2", + "tokio", +] + +[[package]] +name = "mingling_core" +version = "0.3.0" +dependencies = [ + "just_fmt 0.2.0", + "mingling_pathf", +] + +[[package]] +name = "mingling_macros" +version = "0.3.0" +dependencies = [ + "just_fmt 0.2.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "mingling_pathf" +version = "0.3.0" +dependencies = [ + "just_fmt 0.2.0", + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.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 = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.2", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[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 = "syn" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.2", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/mingling_cli/Cargo.toml b/mingling_cli/Cargo.toml new file mode 100644 index 0000000..5b36c6b --- /dev/null +++ b/mingling_cli/Cargo.toml @@ -0,0 +1,60 @@ +[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"] + +[[bin]] +name = "mling" +path = "src/main.rs" + +[dependencies.mingling] +path = "../mingling" +features = [ + "extra_macros", + "picker", + "pathf", + "async", +] + +[build-dependencies.mingling] +path = "../mingling" +features = [ + "builds", + "pathf", +] + +[dependencies] + +# Project analyze +cargo_metadata = { version = "0.23.1", features = ["builder"] } +annotate-snippets = "0.12.16" + +# Code analyze +proc-macro2 = { version = "1.0.107", features = ["span-locations"] } +syn = { version = "3.0.2", features = ["full", "extra-traits"] } +quote = "1.0.47" + +# Configure & Serialization +serde = { version = "1.0.229", features = ["derive"] } +serde_json = "1.0.151" + +# Parallelism +tokio = { version = "1.53.1", features = ["full"] } + +[build-dependencies] +# Configure & Serialization +serde = { version = "1.0.229", features = ["derive"] } +serde_json = "1.0.151" + +# Code gen +just_template = "0.2.0" + +[workspace] diff --git a/mingling_cli/build.rs b/mingling_cli/build.rs new file mode 100644 index 0000000..ec4f208 --- /dev/null +++ b/mingling_cli/build.rs @@ -0,0 +1,12 @@ +use mingling::build::analyze_and_build_type_mapping; + +pub mod pre; + +fn main() { + // Perform path analysis and build type mapping table + analyze_and_build_type_mapping().ok(); + + // Generate lint registry + pre::gen_mod_file().unwrap(); + pre::gen_lint_registry().unwrap(); +} diff --git a/mingling_cli/pre/lint_registry.rs b/mingling_cli/pre/lint_registry.rs new file mode 100644 index 0000000..e69592b --- /dev/null +++ b/mingling_cli/pre/lint_registry.rs @@ -0,0 +1,208 @@ +use std::{fs, io::Error}; + +use just_template::{Template, tmpl}; + +/// Generate lint module registry file (src/lints/mod.rs) +/// +/// Read all Rust source files in the src/lints/ directory (excluding mod.rs itself), +/// automatically generate module declarations and pub use export statements, and write them to mod.rs. +pub fn gen_mod_file() -> Result<(), Error> { + let root = std::env::current_dir()?; + let lints_dir = root.join("src").join("lints"); + let tmpl_file = root.join("tmpls").join("lints.tmpl"); + let mod_file = root.join("src").join("lints.rs"); + + let mut template = Template::from(fs::read_to_string(tmpl_file).unwrap()); + + // Collect all .rs file names (without extension), excluding mod + let mut entries: Vec<String> = fs::read_dir(&lints_dir)? + .filter_map(|e| e.ok()) + .filter(|e| e.path().is_file()) + .filter_map(|e| { + e.path() + .file_stem() + .and_then(|s| s.to_str()) + .map(|s| s.to_string()) + }) + .filter(|name| name != "mod") + .collect(); + + entries.sort(); + + // Generate module declarations and re-export statements + for name in &entries { + tmpl!(template, impls { + mod_name = name + }) + } + + // Generate run_all_lints call arms + for name in &entries { + tmpl!(template, calls { + name = name + }) + } + + // Generate file-level lint calls (lints that have `pub fn check_file`) + for name in &entries { + let file_path = lints_dir.join(format!("{name}.rs")); + let has_check_file = fs::read_to_string(&file_path) + .map(|c| c.contains("pub fn check_file(")) + .unwrap_or(false); + if has_check_file { + tmpl!(template, file_calls { + name = name + }); + tmpl!(template, file_lints { + name = name + }); + } + } + + fs::write(&mod_file, template.to_string())?; + + Ok(()) +} + +/// Generate lint metadata registry +/// +/// Parses each `.rs` file in `src/lints/` (excluding mod.rs and _init.rs), +/// extracts doc-comment metadata, and writes the result as JSON. +pub fn gen_lint_registry() -> Result<(), Error> { + let root = std::env::current_dir()?; + let lints_dir = root.join("src").join("lints"); + + let mut lints: Vec<serde_json::Value> = Vec::new(); + + for entry in fs::read_dir(&lints_dir)? { + let entry = entry?; + let path = entry.path(); + if !path.is_file() || path.extension().and_then(|e| e.to_str()) != Some("rs") { + continue; + } + let stem = match path.file_stem().and_then(|s| s.to_str()) { + Some(s) => s.to_string(), + None => continue, + }; + if stem == "mod" { + continue; + } + + let content = fs::read_to_string(&path)?; + if let Some(meta) = parse_lint_file(&content, &stem) { + lints.push(meta); + } + } + + lints.sort_by(|a, b| { + a["name"] + .as_str() + .unwrap_or("") + .cmp(b["name"].as_str().unwrap_or("")) + }); + + let json = serde_json::json!({ "lints": lints }); + let out_path = root.join("registry.json"); + fs::write(&out_path, serde_json::to_string_pretty(&json).unwrap())?; + Ok(()) +} + +/// Parse a single lint `.rs` file and return its metadata as JSON. +fn parse_lint_file(content: &str, name: &str) -> Option<serde_json::Value> { + // Collect leading doc comment lines (//!) + let mut doc_lines: Vec<String> = Vec::new(); + for line in content.lines() { + let trimmed = line.trim(); + if let Some(rest) = trimmed.strip_prefix("//!") { + doc_lines.push(rest.trim().to_string()); + } else if !doc_lines.is_empty() { + break; + } + } + + // Title: first non-empty doc line + let title = doc_lines + .iter() + .find(|l| !l.is_empty()) + .cloned() + .unwrap_or_default(); + + // Summary: between ## Summary and ## Metadata + let mut summary = String::new(); + let mut in_summary = false; + for line in &doc_lines { + if line.contains("## Summary") { + in_summary = true; + continue; + } + if line.contains("## Metadata") { + in_summary = false; + } + if in_summary && !line.starts_with("## ") { + if !summary.is_empty() { + summary.push('\n'); + } + summary.push_str(line.trim()); + } + } + + // Metadata section + let mut author = String::new(); + let mut default_level = String::from("warn"); + let mut active_on = String::from("File"); + let mut in_metadata = false; + + for line in &doc_lines { + if line.contains("## Metadata") { + in_metadata = true; + continue; + } + if in_metadata { + if line.starts_with("## ") { + break; + } + if let Some(val) = line + .strip_prefix("Author:") + .or_else(|| line.strip_prefix("Author:")) + { + author = val.trim().trim_matches('`').to_string(); + } + if let Some(val) = line + .strip_prefix("Default:") + .or_else(|| line.strip_prefix("Default:")) + { + let raw = val.trim().trim_matches('`'); + if raw == "warn" || raw == "allow" || raw == "deny" { + default_level = raw.to_string(); + } + } + } + } + + // Extract active_on from function signature (last occurrence = actual fn) + if let Some(pos) = content.rfind("pub fn linter(") { + let after = &content[pos..]; + if let Some(colon) = after.find(':') { + let type_part = &after[colon + 1..].trim(); + let raw = type_part + .trim_start_matches("syn::") + .split([' ', ')', ',', '\n']) + .next() + .unwrap_or("File"); + // Strip "Item" prefix: ItemFn → Fn, ItemStruct → Struct, etc. + active_on = raw.strip_prefix("Item").unwrap_or(raw).to_string(); + } + } + + let mut meta = serde_json::Map::new(); + meta.insert("author".into(), serde_json::Value::String(author)); + meta.insert("default".into(), serde_json::Value::String(default_level)); + meta.insert("active_on".into(), serde_json::Value::String(active_on)); + + Some(serde_json::json!({ + "name": name, + "title": title, + "summary": summary.trim(), + "metadata": meta, + })) +} diff --git a/mingling_cli/pre/mod.rs b/mingling_cli/pre/mod.rs new file mode 100644 index 0000000..8eacef4 --- /dev/null +++ b/mingling_cli/pre/mod.rs @@ -0,0 +1,2 @@ +mod lint_registry; +pub use lint_registry::*; diff --git a/mingling_cli/src/diagnostic.rs b/mingling_cli/src/diagnostic.rs new file mode 100644 index 0000000..527e879 --- /dev/null +++ b/mingling_cli/src/diagnostic.rs @@ -0,0 +1,99 @@ +use annotate_snippets::level::{ERROR, HELP, NOTE, WARNING}; +use annotate_snippets::{AnnotationKind, Group, Renderer, Snippet}; +use cargo_metadata::diagnostic::Diagnostic; +use mingling::macros::{buffer, group, r_println, renderer}; + +group!(Diagnostic); + +#[renderer(buffer)] +pub fn render_diagnostic(diagnostic: Diagnostic) { + let report = diagnostic_to_report(&diagnostic); + let renderer = Renderer::styled(); + let rendered = renderer.render(&report); + r_println!("{rendered}"); +} + +fn cargo_level_to_annotate( + level: cargo_metadata::diagnostic::DiagnosticLevel, +) -> &'static annotate_snippets::Level<'static> { + use cargo_metadata::diagnostic::DiagnosticLevel; + match level { + DiagnosticLevel::Ice | DiagnosticLevel::Error => &ERROR, + DiagnosticLevel::Warning => &WARNING, + DiagnosticLevel::Note | DiagnosticLevel::FailureNote => &NOTE, + DiagnosticLevel::Help => &HELP, + _ => &ERROR, + } +} + +/// 把 1-based char offset 转成 0-based byte offset +fn char_offset_to_byte_offset(s: &str, char_offset: usize) -> usize { + s.char_indices() + .nth(char_offset.saturating_sub(1)) + .map(|(i, _)| i) + .unwrap_or(s.len()) +} + +fn diagnostic_to_report<'a>(d: &'a Diagnostic) -> Vec<Group<'a>> { + let level = cargo_level_to_annotate(d.level); + let mut title = level.clone().primary_title(d.message.as_str()); + if let Some(ref code) = d.code { + title = title.id(code.code.as_str()); + } + + let mut group = Group::with_title(title); + + for span in &d.spans { + if !span.is_primary { + continue; + } + + let source: String = span + .text + .iter() + .map(|l| l.text.as_str()) + .collect::<Vec<_>>() + .join("\n"); + + let byte_range = if span.text.len() == 1 { + let line = &span.text[0]; + let start = char_offset_to_byte_offset(&line.text, line.highlight_start); + let end = char_offset_to_byte_offset(&line.text, line.highlight_end); + start..end + } else { + let first = &span.text[0]; + let last = span.text.last().unwrap(); + let start = char_offset_to_byte_offset(&first.text, first.highlight_start); + let prefix_len: usize = span.text[..span.text.len() - 1] + .iter() + .map(|l| l.text.len() + 1) + .sum(); + let end = prefix_len + char_offset_to_byte_offset(&last.text, last.highlight_end); + start..end + }; + + let mut snippet = Snippet::source(source) + .line_start(span.line_start) + .path(span.file_name.as_str()); + + if let Some(ref label) = span.label { + let annotation = AnnotationKind::Primary + .span(byte_range) + .label(label.as_str()); + snippet = snippet.annotation(annotation); + } else { + let annotation = AnnotationKind::Primary.span(byte_range); + snippet = snippet.annotation(annotation); + } + + group = group.element(snippet); + } + + for child in &d.children { + let msg_level = cargo_level_to_annotate(child.level); + let msg = msg_level.clone().message(child.message.as_str()); + group = group.element(msg); + } + + vec![group] +} diff --git a/mingling_cli/src/errors.rs b/mingling_cli/src/errors.rs new file mode 100644 index 0000000..cef9616 --- /dev/null +++ b/mingling_cli/src/errors.rs @@ -0,0 +1 @@ +pub mod serde_json; diff --git a/mingling_cli/src/errors/serde_json.rs b/mingling_cli/src/errors/serde_json.rs new file mode 100644 index 0000000..c73329c --- /dev/null +++ b/mingling_cli/src/errors/serde_json.rs @@ -0,0 +1,8 @@ +use mingling::macros::{buffer, group, r_println, renderer}; + +group!(ErrorSerdeJson = serde_json::Error); + +#[renderer(buffer)] +pub fn render_error_serde_json(_err: ErrorSerdeJson) { + r_println!("serde"); +} diff --git a/mingling_cli/src/linter.rs b/mingling_cli/src/linter.rs new file mode 100644 index 0000000..c85afc1 --- /dev/null +++ b/mingling_cli/src/linter.rs @@ -0,0 +1,67 @@ +use mingling::{ + Program, + macros::{chain, dispatcher, entry, program_setup}, +}; + +use crate::{ + linter::cmd_mlint::{CMDMinglingLinter, EntryMinglingLinter}, + metadata::setup::ResUsingJson, +}; + +pub mod cmd_mlint; +pub mod mlint_attr; +pub mod mlint_report; + +#[program_setup] +pub fn mingling_linter_setup(program: &mut Program<crate::ThisProgram>) { + program.with_setup(MinglingLinterCommandSetup); +} + +#[program_setup] +pub fn mingling_linter_command_setup(program: &mut Program<crate::ThisProgram>) { + program.with_dispatcher(CMDMinglingLinter); + program.with_dispatcher(CMDLinterSupportRustAnalyzer); + program.with_dispatcher(CMDLinterSupportRustAnalyzerWithClippy); + program.with_dispatcher(CMDLinterSupportRustAnalyzerWithCheck); +} + +// Aliases + +dispatcher!("ra-lint-clippy", + CMDLinterSupportRustAnalyzerWithClippy => EntryLinterSupportRustAnalyzerWithClippy +); + +dispatcher!("ra-lint-check", + CMDLinterSupportRustAnalyzerWithCheck => EntryLinterSupportRustAnalyzerWithCheck +); + +dispatcher!("ra-lint", + CMDLinterSupportRustAnalyzer => EntryLinterSupportRustAnalyzer +); + +#[chain] +pub fn handle_ra_lint( + _: EntryLinterSupportRustAnalyzer, + use_json: &mut ResUsingJson, +) -> EntryMinglingLinter { + use_json.using = true; + entry!("--message-format=json") +} + +#[chain] +pub fn handle_ra_lint_check( + _: EntryLinterSupportRustAnalyzerWithCheck, + use_json: &mut ResUsingJson, +) -> EntryMinglingLinter { + use_json.using = true; + entry!("--message-format=json", "--with-checker=cargo,check") +} + +#[chain] +pub fn handle_ra_lint_clippy( + _: EntryLinterSupportRustAnalyzerWithClippy, + use_json: &mut ResUsingJson, +) -> EntryMinglingLinter { + use_json.using = true; + entry!("--message-format=json", "--with-checker=cargo,clippy") +} diff --git a/mingling_cli/src/linter/cmd_mlint.rs b/mingling_cli/src/linter/cmd_mlint.rs new file mode 100644 index 0000000..90871af --- /dev/null +++ b/mingling_cli/src/linter/cmd_mlint.rs @@ -0,0 +1,119 @@ +use crate::linter::mlint_report::{MlintReport, StateLintReports}; +use cargo_metadata::Metadata; +use mingling::LazyRes; +use mingling::consts::REMAINS; +use mingling::macros::{arg, chain, dispatcher, pack}; +use mingling::picker::EntryPicker; + +dispatcher!("lint", CMDMinglingLinter => EntryMinglingLinter); + +/// Main linting function that processes all packages in the metadata. +/// +/// Iterates through all packages and their targets (e.g., binaries, libraries, tests), +/// reads Rust source files (`.rs`), parses them into ASTs, runs lint checks, +/// and enriches each report with metadata information. +async fn linter_main(metadata: &Metadata) -> Vec<MlintReport> { + // Vector to accumulate all lint reports + let mut all_reports = Vec::new(); + + // Iterate over all packages in the metadata + for package in &metadata.packages { + // Iterate over all targets within a package (e.g., bin, lib, test, etc.) + for target in &package.targets { + let path = &target.src_path; + // Only process Rust source files (with `.rs` extension) + if !path.as_str().ends_with(".rs") { + continue; + } + // Read the source file content + let source = match std::fs::read_to_string(path.as_str()) { + Ok(s) => s, + Err(_) => continue, + }; + // Parse the source file into an AST (Abstract Syntax Tree) + let ast = match syn::parse_file(&source) { + Ok(f) => f, + Err(_) => continue, + }; + + // Run all lint checks and collect reports + let reports = crate::lints::run_all_lints(&ast, &source); + // + for mut r in reports { + // Enrich report with metadata information + r.file_name = path.as_str().to_string(); + r.source_code = source.clone(); + r.package_id = Some(package.id.to_string()); + r.target_name = Some(target.name.clone()); + r.target_kind = target.kind.first().map(|k| k.to_string()); + r.target_src_path = Some(path.as_str().to_string()); + all_reports.push(r); + } + } + } + + all_reports +} + +pack!(StateBeginLinter = ()); + +#[chain] +pub fn handle_lint(args: EntryMinglingLinter) -> StateBeginLinter { + let (with_checker, checker_args) = args + .pick_or(&arg![with_checker: Option<String>], || { + Some("cargo,check".to_string()) + }) + .pick(&REMAINS) + .unwrap(); + + // If with_checker is not set, proceed directly to the mingling lint phase + let Some(with_checker) = with_checker else { + return StateBeginLinter::new(()); + }; + + let with_checker: Vec<&str> = with_checker.split(',').collect(); + let checker_args: Vec<String> = checker_args.into(); + + // Run the outer checker (e.g. cargo check) with output passed through directly + execute_checker(&with_checker, checker_args.as_slice()); + + StateBeginLinter::new(()) +} + +/// Run the outer checker (e.g. cargo check) with output passed through directly. +fn execute_checker(with_checker: &[&str], checker_args: &[String]) { + if with_checker.is_empty() { + return; + } + + let checker_str = with_checker.join(" "); + let args_str = checker_args.join(" "); + let full_cmd = if args_str.is_empty() { + checker_str + } else { + format!("{} {}", checker_str, args_str) + }; + + let mut cmd = if cfg!(target_os = "windows") { + let mut c = std::process::Command::new("cmd"); + c.args(["/C", &full_cmd]); + c + } else { + let mut c = std::process::Command::new("sh"); + c.args(["-c", &full_cmd]); + c + }; + + // Pass through stdin/stdout/stderr so the user sees everything + let _ = cmd.status(); +} + +#[chain] +pub async fn handle_state_begin_linter( + _: StateBeginLinter, + metadata: &mut LazyRes<crate::metadata::setup::ResMetadata>, +) -> StateLintReports { + let metadata = metadata.get_ref().data(); + let reports = linter_main(metadata).await; + StateLintReports::new(reports) +} diff --git a/mingling_cli/src/linter/mlint_attr.rs b/mingling_cli/src/linter/mlint_attr.rs new file mode 100644 index 0000000..24528d4 --- /dev/null +++ b/mingling_cli/src/linter/mlint_attr.rs @@ -0,0 +1,39 @@ +/// Result of checking `mlint(allow/warn/deny(...))` attributes. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum MlintLevelOverride { + Allow, + Warn, + Deny, +} + +/// Parse `mlint(allow/warn/deny(lint_name))` from attributes. +/// Returns `None` if the lint is not mentioned in any mlint attribute. +pub fn get_mlint_override(attrs: &[syn::Attribute], lint_name: &str) -> Option<MlintLevelOverride> { + for attr in attrs { + if !attr.path().is_ident("mlint") { + continue; + } + let list = match attr.meta.require_list() { + Ok(l) => l, + Err(_) => continue, + }; + // TokenStream::to_string() adds spaces between tokens, + // e.g. `allow(xxx)` → `allow ( xxx )`. Remove all spaces to compare. + let flat: String = list + .tokens + .to_string() + .chars() + .filter(|c| !c.is_whitespace()) + .collect(); + for (keyword, variant) in [ + ("allow", MlintLevelOverride::Allow), + ("warn", MlintLevelOverride::Warn), + ("deny", MlintLevelOverride::Deny), + ] { + if flat.contains(&format!("{keyword}({lint_name})")) { + return Some(variant); + } + } + } + None +} diff --git a/mingling_cli/src/linter/mlint_report.rs b/mingling_cli/src/linter/mlint_report.rs new file mode 100644 index 0000000..0472056 --- /dev/null +++ b/mingling_cli/src/linter/mlint_report.rs @@ -0,0 +1,445 @@ +use std::ops::Range; + +use cargo_metadata::diagnostic::{ + DiagnosticCodeBuilder, DiagnosticLevel as CargoLevel, DiagnosticSpanBuilder, + DiagnosticSpanLineBuilder, +}; + +use cargo_metadata::{Message, PackageId}; + +use annotate_snippets::level::{ERROR, HELP, NOTE, WARNING}; +use annotate_snippets::{AnnotationKind, Group, Patch, Renderer, Snippet}; +use mingling::macros::{buffer, chain, pack, r_append, r_eprintln, renderer}; +use mingling::{AnyOutput, ProgramCollect, Routable}; + +use crate::Next; +use crate::metadata::setup::ResUsingJson; + +/// Complete structure of a Lint report, containing inspection results and associated metadata. +#[derive(Default)] +pub struct MlintReport { + /// Source file name + pub file_name: String, + + /// Full source text of the file, used to extract line content and compute byte offsets + pub source_code: String, + + /// Severity level of the report + pub level: MlintLevel, + + /// Name of the Lint + pub lint_code: String, + + /// Content of the report + pub message: String, + + /// Source code locations + pub spans: Vec<LintSpan>, + + /// Attached sub-reports for this report + pub attached_reports: Vec<MlintReport>, + + /// Package ID that this report belongs to + pub package_id: Option<String>, + + /// Compilation target name that this report belongs to + pub target_name: Option<String>, + + /// Compilation target type that this report belongs to + pub target_kind: Option<String>, + + /// Compilation target source file path that this report belongs to + pub target_src_path: Option<String>, + + /// Suggestions for automatic fix (shown as diff in annotated output) + pub suggestions: Vec<LintSuggestion>, +} + +/// Report severity level, indicating the seriousness of the Lint result. +#[derive(Default, Clone, Copy, PartialEq, Eq)] +pub enum MlintLevel { + #[default] + Note, + Error, + Warning, + Help, +} + +/// Source code location span, representing a range of source code and its associated text information. +pub struct LintSpan { + /// Starting line number (1-based) + pub line_start: usize, + /// Ending line number (1-based) + pub line_end: usize, + /// Starting column (1-based char offset) + pub column_start: usize, + /// Ending column (1-based char offset) + pub column_end: usize, + /// Source lines at this location + pub text: Vec<LintSpanLine>, + /// Optional label description + pub label: Option<String>, +} + +/// A single line of text in a source location with highlight range. +pub struct LintSpanLine { + /// Full source line text (no trailing `\n`) + pub text: String, + /// Highlight start (1-based char offset) + pub highlight_start: usize, + /// Highlight end (1-based char offset) + pub highlight_end: usize, +} + +/// A suggestion shown as a diff in the output (e.g. `- old code` / `+ new code`). +#[derive(Clone, Debug, Default)] +pub struct LintSuggestion { + /// Source text that the suggestion applies to (a single line or snippet) + pub source: String, + /// Line number where the suggestion applies + pub line_start: usize, + /// Byte range within `source` to replace + pub byte_range: Range<usize>, + /// Replacement text + pub replacement: String, +} + +impl MlintReport { + /// Build a `LintSpan` from a syn spanned item and the source text. + pub fn span_from_syn<T: syn::spanned::Spanned>(value: &T, source: &str) -> LintSpan { + let span = value.span(); + let start = span.start(); + let end = span.end(); + + // Extract line content from source + let lines: Vec<&str> = source.lines().collect(); + let text = if start.line == end.line && start.line <= lines.len() { + let line_text = lines[start.line.saturating_sub(1)]; + let hl_start = proc_macro2_byte_col_to_char_1based(line_text, start.column); + let hl_end = proc_macro2_byte_col_to_char_1based(line_text, end.column); + vec![LintSpanLine { + text: line_text.to_string(), + highlight_start: hl_start, + highlight_end: hl_end, + }] + } else { + // Multi-line: generate line by line + (start.line..=end.line.min(lines.len())) + .map(|i| { + let line_text = lines[i.saturating_sub(1)]; + let (hl_start, hl_end) = if i == start.line { + ( + proc_macro2_byte_col_to_char_1based(line_text, start.column), + line_text.chars().count(), + ) + } else if i == end.line { + ( + 1, + proc_macro2_byte_col_to_char_1based(line_text, end.column), + ) + } else { + (1, line_text.chars().count()) + }; + LintSpanLine { + text: line_text.to_string(), + highlight_start: hl_start, + highlight_end: hl_end, + } + }) + .collect::<Vec<_>>() + }; + + LintSpan { + line_start: start.line, + line_end: end.line, + column_start: proc_macro2_byte_col_to_char_1based( + lines.get(start.line.saturating_sub(1)).unwrap_or(&""), + start.column, + ), + column_end: proc_macro2_byte_col_to_char_1based( + lines.get(end.line.saturating_sub(1)).unwrap_or(&""), + end.column, + ), + text, + label: None, + } + } + + /// Compute byte offset from (line, column) within source. + /// line: 1-based, column: 1-based char offset. + pub fn line_col_to_byte_offset(&self, line: usize, col: usize) -> usize { + let mut byte_pos = 0usize; + for (i, line_str) in self.source_code.lines().enumerate() { + if i + 1 == line { + return byte_pos + char_1based_to_byte_offset(line_str, col); + } + byte_pos += line_str.len() + 1; // +1 for \n + } + self.source_code.len() + } +} + +/// proc-macro2's LineColumn.column is **0-based byte offset**. +/// Convert to 1-based char offset. +fn proc_macro2_byte_col_to_char_1based(line: &str, byte_col: usize) -> usize { + line.char_indices() + .position(|(i, _)| i >= byte_col) + .map(|pos| pos + 1) // → 1-based + .unwrap_or(line.chars().count().max(1)) +} + +/// 1-based char offset → byte offset within a string +fn char_1based_to_byte_offset(s: &str, char_1based: usize) -> usize { + s.char_indices() + .nth(char_1based.saturating_sub(1)) + .map(|(i, _)| i) + .unwrap_or(s.len()) +} + +impl MlintReport { + pub fn to_annotate_snippet_render(&self) -> String { + let level = mlinit_level_to_annotate(&self.level); + let title = level.clone().primary_title(&self.message); + // code 不放在 title 里,改放在 note 中 + let mut group = Group::with_title(title); + + for span in &self.spans { + let source = span + .text + .iter() + .map(|l| l.text.as_str()) + .collect::<Vec<_>>() + .join("\n"); + + let byte_range = if span.text.len() == 1 { + let line = &span.text[0]; + let start = char_1based_to_byte_offset(&line.text, line.highlight_start); + let end = char_1based_to_byte_offset(&line.text, line.highlight_end); + start..end + } else { + let first = &span.text[0]; + let last = span.text.last().unwrap(); + let start = char_1based_to_byte_offset(&first.text, first.highlight_start); + let prefix_len: usize = span.text[..span.text.len() - 1] + .iter() + .map(|l| l.text.len() + 1) + .sum(); + let end = prefix_len + char_1based_to_byte_offset(&last.text, last.highlight_end); + start..end + }; + + let mut snippet = Snippet::source(source) + .line_start(span.line_start) + .path(&self.file_name); + + let annotation = match &span.label { + Some(label) => AnnotationKind::Primary + .span(byte_range) + .label(label.as_str()), + None => AnnotationKind::Primary.span(byte_range), + }; + snippet = snippet.annotation(annotation); + group = group.element(snippet); + } + + for child in &self.attached_reports { + let child_level = mlinit_level_to_annotate(&child.level); + let msg = child_level.clone().message(&child.message); + group = group.element(msg); + } + + // Render suggestions as diffs + for sugg in &self.suggestions { + let patch_snippet = Snippet::source(sugg.source.clone()) + .line_start(sugg.line_start) + .path(&self.file_name) + .patch(Patch::new( + sugg.byte_range.clone(), + sugg.replacement.clone(), + )); + group = group.element(patch_snippet); + } + + if !self.lint_code.is_empty() { + let level_name = match self.level { + MlintLevel::Error => "deny", + MlintLevel::Warning => "warn", + MlintLevel::Note | MlintLevel::Help => "allow", + }; + let note_text = format!("`#[mlint({level_name}({}))]` on by default", self.lint_code); + let note_msg = NOTE.clone().message(note_text); + group = group.element(note_msg); + } + + let renderer = Renderer::styled(); + renderer.render(&[group]) + } +} + +fn mlinit_level_to_annotate(level: &MlintLevel) -> &'static annotate_snippets::Level<'static> { + match level { + MlintLevel::Error => &ERROR, + MlintLevel::Warning => &WARNING, + MlintLevel::Note => &NOTE, + MlintLevel::Help => &HELP, + } +} + +impl MlintReport { + pub fn to_compiler_message(&self) -> Message { + use cargo_metadata::{CompilerMessageBuilder, Edition, TargetBuilder}; + + let target_kind_str = self.target_kind.as_deref().unwrap_or("bin"); + let target_kind_parsed: cargo_metadata::TargetKind = target_kind_str.into(); + let crate_kind: cargo_metadata::CrateType = target_kind_str.into(); + + let target = TargetBuilder::default() + .name(self.target_name.as_deref().unwrap_or_default()) + .kind(vec![target_kind_parsed]) + .crate_types(vec![crate_kind]) + .required_features(Vec::<String>::new()) + .src_path(self.target_src_path.as_deref().unwrap_or_default()) + .edition(Edition::E2021) + .doctest(false) + .test(false) + .doc(false) + .build() + .unwrap(); + + let diagnostic = self.build_diagnostic( + &self.message, + &self.lint_code, + &self.level, + &self.spans, + &self.attached_reports, + ); + + Message::CompilerMessage( + CompilerMessageBuilder::default() + .package_id(PackageId { + repr: self.package_id.clone().unwrap_or_else(|| "unknown".into()), + }) + .target(target) + .message(diagnostic) + .build() + .unwrap(), + ) + } + + fn build_diagnostic( + &self, + message: &str, + code: &str, + level: &MlintLevel, + spans: &[LintSpan], + children: &[MlintReport], + ) -> cargo_metadata::diagnostic::Diagnostic { + cargo_metadata::diagnostic::DiagnosticBuilder::default() + .message(message) + .code( + DiagnosticCodeBuilder::default() + .code(code) + .explanation(None) + .build() + .unwrap(), + ) + .level(match level { + MlintLevel::Error => CargoLevel::Error, + MlintLevel::Warning => CargoLevel::Warning, + MlintLevel::Note => CargoLevel::Note, + MlintLevel::Help => CargoLevel::Help, + }) + .spans( + spans + .iter() + .map(|s| self.lint_span_to_diagnostic_span(s)) + .collect::<Vec<_>>(), + ) + .children( + children + .iter() + .map(|c| { + self.build_diagnostic( + &c.message, + &c.lint_code, + &c.level, + &c.spans, + &c.attached_reports, + ) + }) + .collect::<Vec<_>>(), + ) + .rendered(None) + .build() + .unwrap() + } + + fn lint_span_to_diagnostic_span( + &self, + span: &LintSpan, + ) -> cargo_metadata::diagnostic::DiagnosticSpan { + let byte_start = self.line_col_to_byte_offset(span.line_start, span.column_start); + let byte_end = self.line_col_to_byte_offset(span.line_end, span.column_end); + + DiagnosticSpanBuilder::default() + .file_name(&self.file_name) + .byte_start(byte_start as u32) + .byte_end(byte_end as u32) + .line_start(span.line_start) + .line_end(span.line_end) + .column_start(span.column_start) + .column_end(span.column_end) + .is_primary(true) + .text( + span.text + .iter() + .map(|l| { + DiagnosticSpanLineBuilder::default() + .text(&l.text) + .highlight_start(l.highlight_start) + .highlight_end(l.highlight_end) + .build() + .unwrap() + }) + .collect::<Vec<_>>(), + ) + .label(span.label.clone()) + .suggested_replacement(self.suggestions.first().map(|s| s.replacement.clone())) + .suggestion_applicability(if !self.suggestions.is_empty() { + Some(cargo_metadata::diagnostic::Applicability::MachineApplicable) + } else { + None + }) + .expansion(None) + .build() + .unwrap() + } +} + +pack!(StateLintReports = Vec<MlintReport>); +pack!(ResultLintReportsAnnotateSnippet = Vec<MlintReport>); +pack!(ResultLintReportsJson = Vec<MlintReport>); + +#[chain] +pub fn handle_state_lint_reports(reports: StateLintReports, using_json: &ResUsingJson) -> Next { + if using_json.using { + ResultLintReportsJson::new(reports.inner).to_render() + } else { + ResultLintReportsAnnotateSnippet::new(reports.inner).to_render() + } +} + +#[renderer(buffer)] +pub fn render_lint_reports(reports: ResultLintReportsAnnotateSnippet) { + for report in reports.inner { + r_eprintln!("{}", report.to_annotate_snippet_render()); + } +} + +#[renderer(buffer)] +pub fn render_lint_reports_json(reports: ResultLintReportsJson) { + for report in reports.inner { + // DIRTY: Dispatch to the Message renderer using AnyOutput to obtain the render result and append it to the Buffer + r_append!(|| { crate::ThisProgram::render(AnyOutput::new(report.to_compiler_message())) }); + } +} diff --git a/mingling_cli/src/lints.rs b/mingling_cli/src/lints.rs new file mode 100644 index 0000000..89f5ca3 --- /dev/null +++ b/mingling_cli/src/lints.rs @@ -0,0 +1,82 @@ +#![allow(unused)] +use crate::linter::mlint_report::{MlintLevel, MlintReport}; + +mod non_mingling_naming_style; +pub use non_mingling_naming_style::linter as non_mingling_naming_style; +mod template_linter; +pub use template_linter::linter as template_linter; +mod unnecessary_render_result_creation; +pub use unnecessary_render_result_creation::linter as unnecessary_render_result_creation; +pub use non_mingling_naming_style::check_file as non_mingling_naming_style_file; + +/// Run all registered lints on a parsed file with its source text. +pub fn run_all_lints(file: &syn::File, source: &str) -> Vec<MlintReport> { + use crate::linter::mlint_attr::{get_mlint_override, MlintLevelOverride}; + + // File-level lints (check types, modules, etc.) + let mut reports: Vec<MlintReport> = vec![]; + reports.extend(non_mingling_naming_style::check_file(file, source)); + + // Item-level lints (check each function) + for item in &file.items { + if let syn::Item::Fn(f) = item { + let skip = get_mlint_override(&f.attrs, "non_mingling_naming_style") == Some(MlintLevelOverride::Allow); + if !skip { + let mut rs = non_mingling_naming_style::linter(f.clone(), source); + if get_mlint_override(&f.attrs, "non_mingling_naming_style") == Some(MlintLevelOverride::Deny) { + for r in &mut rs { r.level = MlintLevel::Error; } + } + reports.extend(rs); + } + let skip = get_mlint_override(&f.attrs, "template_linter") == Some(MlintLevelOverride::Allow); + if !skip { + let mut rs = template_linter::linter(f.clone(), source); + if get_mlint_override(&f.attrs, "template_linter") == Some(MlintLevelOverride::Deny) { + for r in &mut rs { r.level = MlintLevel::Error; } + } + reports.extend(rs); + } + let skip = get_mlint_override(&f.attrs, "unnecessary_render_result_creation") == Some(MlintLevelOverride::Allow); + if !skip { + let mut rs = unnecessary_render_result_creation::linter(f.clone(), source); + if get_mlint_override(&f.attrs, "unnecessary_render_result_creation") == Some(MlintLevelOverride::Deny) { + for r in &mut rs { r.level = MlintLevel::Error; } + } + reports.extend(rs); + } + } + } + + // Apply file-level #![mlint(allow/warn/deny(...))] overrides + for r in &mut reports { + if let Some(override_level) = get_mlint_override(&file.attrs, &r.lint_code) { + match override_level { + MlintLevelOverride::Allow => r.level = MlintLevel::Help, + MlintLevelOverride::Deny => r.level = MlintLevel::Error, + MlintLevelOverride::Warn => { + if r.level != MlintLevel::Error { r.level = MlintLevel::Warning; } + } + } + } + } + reports.retain(|r| r.level != MlintLevel::Help); + reports +} + +#[macro_export] +macro_rules! assert_detected { + ($linter:expr, $ast_type:ty => { $($code:tt)* }) => { + let source = stringify!($($code)*); + let ast: $ast_type = syn::parse_str(&source).unwrap(); + assert!(!$linter(ast, &source).is_empty()); + }; +} + +#[macro_export] +macro_rules! assert_not_detected { + ($linter:expr, $ast_type:ty => { $($code:tt)* }) => { + let source = stringify!($($code)*); + let ast: $ast_type = syn::parse_str(&source).unwrap(); + assert!($linter(ast, &source).is_empty()); + }; +}
\ No newline at end of file diff --git a/mingling_cli/src/lints/non_mingling_naming_style.rs b/mingling_cli/src/lints/non_mingling_naming_style.rs new file mode 100644 index 0000000..83168b7 --- /dev/null +++ b/mingling_cli/src/lints/non_mingling_naming_style.rs @@ -0,0 +1,352 @@ +//! Non-Mingling Naming Style +//! +//! ## Summary +//! +//! Checks that Mingling functions follow naming conventions: +//! +//! | Prefix | 1st param must be | +//! |--------|------------------| +//! | `handle_` | `Entry*` | +//! | `handle_state_` | `State*` | +//! | `handle_error_` | `Error*` | +//! | `help_` | `Entry*` | +//! | `render_` | `Result*` | +//! | `render_error_` | `Error*` | +//! +//! The name after prefix (snake_case) must match the type after prefix (PascalCase). +//! +//! ## Metadata +//! +//! Author: `Weicao-CatilGrass` +//! Default: `warn` + +use crate::linter::mlint_report::{LintSuggestion, MlintLevel, MlintReport}; +use quote::ToTokens; +use syn::spanned::Spanned; + +/// File-level entry (placeholder). +pub fn check_file(_file: &syn::File, _source: &str) -> Vec<MlintReport> { + vec![] +} + +/// ItemFn entry. +pub fn linter(ast: syn::ItemFn, source: &str) -> Vec<MlintReport> { + check_fn_name(&ast, source) +} + +fn check_fn_name(func: &syn::ItemFn, source: &str) -> Vec<MlintReport> { + // Only check functions with Mingling attributes + let has_mingling_attr = func.attrs.iter().any(|a| { + let name = a.path().to_token_stream().to_string(); + name.ends_with("renderer") + || name.ends_with("chain") + || name.ends_with("help") + || name.ends_with("completion") + }); + if !has_mingling_attr { + return vec![]; + } + + let name = func.sig.ident.to_string(); + let first_param = func.sig.inputs.first(); + + let rule: Option<(&str, &str)> = { + if name.starts_with("handle_state_") { + Some(("handle_state_", "State")) + } else if name.starts_with("handle_error_") { + Some(("handle_error_", "Error")) + } else if name.starts_with("handle_") { + Some(("handle_", "Entry")) + } else if name.starts_with("render_error_") { + Some(("render_error_", "Error")) + } else if name.starts_with("render_") { + Some(("render_", "Result")) + } else if name.starts_with("help_") { + Some(("help_", "Entry")) + } else { + None + } + }; + + let Some((prefix, expected_prefix)) = rule else { + return vec![]; + }; + let fn_rest = &name[prefix.len()..]; + + let Some(first) = first_param else { + return vec![MlintReport { + level: MlintLevel::Warning, + lint_code: "non_mingling_naming_style".into(), + message: format!( + "`{name}` should take `{expected_prefix}*` as its first parameter, but it has no parameters" + ), + ..Default::default() + }]; + }; + + let type_name = param_type_name(first); + + if !type_name.starts_with(expected_prefix) || type_name.len() <= expected_prefix.len() { + let expected_type = format!("{expected_prefix}{}", snake_to_pascal(fn_rest)); + return vec![MlintReport { + level: MlintLevel::Warning, + lint_code: "non_mingling_naming_style".into(), + message: format!( + "`{name}` should take `{expected_prefix}*`, but got `{type_name}` — rename it to `{expected_type}`" + ), + spans: vec![MlintReport::span_from_syn(&func.sig.ident, source)], + ..Default::default() + }]; + } + + let type_rest = &type_name[expected_prefix.len()..]; + let fn_rest_normalized = snake_to_pascal(fn_rest); + + if type_rest != fn_rest_normalized { + let expected_type = format!("{expected_prefix}{fn_rest_normalized}"); + let expected_fn = format!("{prefix}{}", pascal_to_snake(type_rest)); + + // Heuristic: if the type's base name is a substring of fn_rest, the type is clean → rename fn + let rename_fn = fn_rest.to_lowercase().contains(&type_rest.to_lowercase()) + && fn_rest.to_lowercase() != type_rest.to_lowercase(); + + let (msg, span) = if rename_fn { + ( + format!( + "naming mismatch: rename `{name}` to `{expected_fn}` to match type `{type_name}`" + ), + MlintReport::span_from_syn(&func.sig.ident, source), + ) + } else { + ( + format!( + "naming mismatch: rename type `{type_name}` to `{expected_type}` to match function `{name}`" + ), + first_param_type_span(first, source), + ) + }; + + // Build diff suggestion + let source_line = source + .lines() + .nth(func.sig.span().start().line.saturating_sub(1)) + .unwrap_or(""); + let (byte_range_start, suggestion_target) = if rename_fn { + // Find the old function name in the source line + let pos = source_line.find(&name).unwrap_or(0); + (pos, expected_fn.clone()) + } else { + // Find the old type name in the source line + let pos = source_line.find(&type_name).unwrap_or(0); + (pos, expected_type.clone()) + }; + let byte_range = byte_range_start + ..byte_range_start + + if rename_fn { + name.len() + } else { + type_name.len() + }; + + return vec![MlintReport { + level: MlintLevel::Warning, + lint_code: "non_mingling_naming_style".into(), + message: msg, + spans: vec![span], + suggestions: vec![LintSuggestion { + source: source_line.to_string(), + line_start: func.sig.span().start().line, + byte_range, + replacement: suggestion_target, + }], + attached_reports: vec![MlintReport { + level: MlintLevel::Help, + message: format!("expected `{expected_fn}` ↔ `{expected_type}`"), + ..Default::default() + }], + ..Default::default() + }]; + } + + vec![] +} + +fn param_type_name(arg: &syn::FnArg) -> String { + if let syn::FnArg::Typed(pat) = arg + && let syn::Type::Path(ref tp) = *pat.ty.clone() + { + return tp + .path + .segments + .iter() + .map(|s| s.ident.to_string()) + .collect::<Vec<_>>() + .join("::"); + } + String::new() +} + +fn first_param_type_span(arg: &syn::FnArg, source: &str) -> crate::linter::mlint_report::LintSpan { + if let syn::FnArg::Typed(pat) = arg { + return MlintReport::span_from_syn(&*pat.ty, source); + } + MlintReport::span_from_syn(arg, source) +} + +fn snake_to_pascal(s: &str) -> String { + let mut r = String::new(); + let mut cap = true; + for ch in s.chars() { + if ch == '_' { + cap = true; + } else if cap { + r.extend(ch.to_uppercase()); + cap = false; + } else { + r.push(ch); + } + } + r +} + +fn pascal_to_snake(s: &str) -> String { + let mut r = String::new(); + for (i, ch) in s.char_indices() { + if ch.is_uppercase() && i != 0 { + r.push('_'); + } + for lower in ch.to_lowercase() { + r.push(lower); + } + } + r +} + +#[cfg(test)] +mod lint_test { + use crate::{assert_detected, assert_not_detected}; + + #[test] + fn handle_entry() { + assert_not_detected!(super::linter, syn::ItemFn => { + #[::mingling::macros::chain] + fn handle_greet(args: EntryGreet) {} + }); + } + + #[test] + fn handle_state() { + assert_not_detected!(super::linter, syn::ItemFn => { + #[::mingling::macros::chain] + fn handle_state_processing(prev: StateProcessing) {} + }); + } + + #[test] + fn handle_error() { + assert_not_detected!(super::linter, syn::ItemFn => { + #[::mingling::macros::chain] + fn handle_error_not_found(err: ErrorNotFound) {} + }); + } + + #[test] + fn render_result() { + assert_not_detected!(super::linter, syn::ItemFn => { + #[::mingling::macros::renderer] + fn render_greeting(result: ResultGreeting) {} + }); + } + + #[test] + fn render_error() { + assert_not_detected!(super::linter, syn::ItemFn => { + #[::mingling::macros::renderer] + fn render_error_not_found(err: ErrorNotFound) {} + }); + } + + #[test] + fn help_entry() { + assert_not_detected!(super::linter, syn::ItemFn => { + #[::mingling::macros::help] + fn help_greet(args: EntryGreet) {} + }); + } + + #[test] + fn handle_should_be_entry() { + assert_detected!(super::linter, syn::ItemFn => { + #[::mingling::macros::chain] + fn handle_greet(x: String) {} + }); + } + + #[test] + fn handle_state_should_be_state() { + assert_detected!(super::linter, syn::ItemFn => { + #[::mingling::macros::chain] + fn handle_state_processing(x: String) {} + }); + } + + #[test] + fn handle_error_should_be_error() { + assert_detected!(super::linter, syn::ItemFn => { + #[::mingling::macros::chain] + fn handle_error_not_found(x: String) {} + }); + } + + #[test] + fn render_should_be_result() { + assert_detected!(super::linter, syn::ItemFn => { + #[::mingling::macros::renderer] + fn render_greeting(x: EntryGreet) {} + }); + } + + #[test] + fn render_error_should_be_error() { + assert_detected!(super::linter, syn::ItemFn => { + #[::mingling::macros::renderer] + fn render_error_not_found(x: ResultGreeting) {} + }); + } + + #[test] + fn name_mismatch_rename_fn() { + assert_detected!(super::linter, syn::ItemFn => { + #[::mingling::macros::renderer] + fn render_result_greeting(_greeting: ResultGreeting) {} + }); + } + + #[test] + fn name_mismatch_rename_type() { + assert_detected!(super::linter, syn::ItemFn => { + #[::mingling::macros::chain] + fn handle_greet(args: EntryHello) {} + }); + } + + #[test] + fn handle_no_params() { + assert_detected!(super::linter, syn::ItemFn => { + #[::mingling::macros::chain] + fn handle_greet() {} + }); + } + + #[test] + fn regular_fn_ok() { + assert_not_detected!(super::linter, syn::ItemFn => { + fn do_something(x: i32) {} + }); + } + + #[test] + fn handle_no_params_and_no_attrs() { + assert_not_detected!(super::linter, syn::ItemFn => { fn handle_greet() {} }); + } +} diff --git a/mingling_cli/src/lints/template_linter.rs b/mingling_cli/src/lints/template_linter.rs new file mode 100644 index 0000000..4836e24 --- /dev/null +++ b/mingling_cli/src/lints/template_linter.rs @@ -0,0 +1,56 @@ +//! Template Linter +//! +//! ## Summary +//! +//! This is a template Linter that introduces how to add a Lint for Mingling. +//! You can write an introduction for this Linter in the Summary section, for example: +//! +//! - Trigger conditions +//! - Why is it necessary? +//! +//! ## Metadata +//! +//! > This section is the **Metadata** section, which needs to be filled in correctly. +//! > These contents will eventually be compiled as the Linter's behavior. +//! +//! Author: `Your-Name` +//! Default: `allow` +// ^^^^^ Supported parameters: `warn`, `allow`, `deny` + +// --- ABOUT AUTO IDENTIFICATION RULES --- +// +// The compiler will treat code with the following structure as a Linter entry point: +// | +// --> your_linter_module.rs +// | +// | pub fn linter(ast: syn::ItemX) -> Vec<MlintReport> { +// | /* ... */ ^^^^^^^^^^ Your linter scope +// | } +// | +// = note: Please ensure your function is `pub`, named `linter`, and returns `Vec<MlintReport>` +// +// --- ABOUT AUTO IDENTIFICATION RULES --- + +use crate::linter::mlint_report::MlintReport; + +pub fn linter(_ast: syn::ItemFn, _source: &str) -> Vec<MlintReport> { + // ^^^^^^^^^^^ Supported parameters: + // | syn::File + // | syn::ItemImpl + // | syn::ItemStruct + // | syn::ItemEnum + // | syn::ItemTrait + // | syn::ItemFn + // | syn::ItemMacro + // | syn::ItemMod + // | syn::ItemUnion + vec![] +} + +#[cfg(test)] +mod lint_test { + use crate::{assert_detected, assert_not_detected}; + + #[test] + fn test() {} +} diff --git a/mingling_cli/src/lints/unnecessary_render_result_creation.rs b/mingling_cli/src/lints/unnecessary_render_result_creation.rs new file mode 100644 index 0000000..1450c1a --- /dev/null +++ b/mingling_cli/src/lints/unnecessary_render_result_creation.rs @@ -0,0 +1,490 @@ +//! Unnecessary Manual RenderResult Creation +//! +//! ## Summary +//! +//! Detects `#[renderer]` functions that manually create a `RenderResult` and +//! manage it via `r_println!(r, ...)` style calls, when they could be simplified +//! to `#[renderer(buffer)]` which handles the buffer automatically. +//! +//! This lint will not trigger if `r` is used outside of `r_println!`, +//! `r_eprintln!`, `r_print`, `r_eprint`, or `r_append`. +//! +//! ## Metadata +//! +//! Author: `Weicao-CatilGrass` +//! Default: `warn` + +use crate::linter::mlint_report::{LintSuggestion, MlintLevel, MlintReport}; +use quote::ToTokens; +use syn::spanned::Spanned; + +pub fn linter(ast: syn::ItemFn, source: &str) -> Vec<MlintReport> { + if !has_renderer_attr(&ast) { + return vec![]; + } + + let stmts = &ast.block.stmts; + if stmts.len() < 2 { + return vec![]; + } + + let (r_ident, let_idx) = match find_render_result_new(stmts) { + Some(pair) => pair, + None => return vec![], + }; + + let r_name = r_ident.to_string(); + let mut only_print_and_return = true; + let mut print_count = 0; + + for stmt in &stmts[let_idx + 1..] { + if !check_stmt_usage(stmt, &r_name, &mut print_count) { + only_print_and_return = false; + break; + } + } + + if only_print_and_return && print_count > 0 { + let span = MlintReport::span_from_syn(&ast.sig, source); + let mut suggestions = Vec::new(); + + // 1. Attribute change: #[renderer] → #[renderer(buffer)] + if let Some(sugg) = make_attr_suggestion(&ast, source) { + suggestions.push(sugg); + } + + // 2. Remove -> RenderResult from function signature + if let Some(sugg) = make_return_type_suggestion(&ast, source) { + suggestions.push(sugg); + } + + // 3. Remove let mut r = RenderResult::... + if let Some(sugg) = make_let_removal_suggestion(stmts, let_idx, source) { + suggestions.push(sugg); + } + + // 4. Fix r_println!(r, ...) → r_println!(...) for all r_xxx macros + suggestions.extend(make_macro_arg_suggestions(stmts, &r_name, source)); + + // 5. Remove return 'r' expression + if let Some(sugg) = make_return_removal_suggestion(stmts, &r_name, source) { + suggestions.push(sugg); + } + + vec![MlintReport { + source_code: source.to_string(), + level: MlintLevel::Warning, + lint_code: "unnecessary_render_result_creation".into(), + message: format!( + "unnecessary manual `RenderResult` creation in `{}`: use `#[renderer(buffer)]` instead", + ast.sig.ident, + ), + spans: vec![span], + suggestions, + attached_reports: vec![MlintReport { + level: MlintLevel::Help, + message: format!( + "change to `#[renderer(buffer)]` and use `r_println!(...)` without the `{}` parameter", + r_name, + ), + ..Default::default() + }], + ..Default::default() + }] + } else { + vec![] + } +} + +fn has_renderer_attr(func: &syn::ItemFn) -> bool { + func.attrs.iter().any(|a| { + if !a.path().is_ident("renderer") { + return false; + } + if let Ok(list) = a.meta.require_list() { + let tokens = list.tokens.to_string(); + if tokens.contains("buffer") { + return false; + } + } + true + }) +} + +fn find_render_result_new(stmts: &[syn::Stmt]) -> Option<(proc_macro2::Ident, usize)> { + for (i, stmt) in stmts.iter().enumerate() { + if let syn::Stmt::Local(local) = stmt + && let Some(init) = &local.init + && let syn::Pat::Ident(pat_id) = &local.pat + && pat_id.mutability.is_some() + && let syn::Expr::Call(call) = &*init.expr + && let syn::Expr::Path(expr_path) = call.func.as_ref() + { + let segs = &expr_path.path.segments; + let matches = match segs.len() { + 2 => { + segs[0].ident == "RenderResult" + && (segs[1].ident == "new" || segs[1].ident == "default") + } + 3 => { + segs[0].ident == "mingling" + && segs[1].ident == "RenderResult" + && (segs[2].ident == "new" || segs[2].ident == "default") + } + _ => false, + }; + if matches { + return Some((pat_id.ident.clone(), i)); + } + } + + // Also handle `RenderResult::from(...)` and `mingling::RenderResult::from(...)` + if let syn::Stmt::Local(local) = stmt + && let Some(init) = &local.init + && let syn::Pat::Ident(pat_id) = &local.pat + && pat_id.mutability.is_some() + && let syn::Expr::Call(call) = &*init.expr + && let syn::Expr::Path(expr_path) = call.func.as_ref() + { + let segs = &expr_path.path.segments; + let matches = match segs.len() { + 2 => segs[0].ident == "RenderResult" && segs[1].ident == "from", + 3 => { + segs[0].ident == "mingling" + && segs[1].ident == "RenderResult" + && segs[2].ident == "from" + } + _ => false, + }; + if matches { + return Some((pat_id.ident.clone(), i)); + } + } + } + None +} + +fn check_stmt_usage(stmt: &syn::Stmt, r_name: &str, print_count: &mut usize) -> bool { + // return r; → allowed + if let syn::Stmt::Expr(expr, _) = stmt + && let syn::Expr::Return(ret) = expr + && let Some(ret_expr) = &ret.expr + && let syn::Expr::Path(p) = ret_expr.as_ref() + && p.path.is_ident(r_name) + { + return true; + } + + // r_println!(r, ...) → allowed + if let syn::Stmt::Macro(stmt_mac) = stmt { + let macro_name = stmt_mac + .mac + .path + .segments + .last() + .map(|s| s.ident.to_string()) + .unwrap_or_default(); + let is_r_macro = matches!( + macro_name.as_str(), + "r_println" | "r_eprintln" | "r_print" | "r_eprint" | "r_append" + ); + if is_r_macro + && let Some(first_arg) = stmt_mac.mac.tokens.clone().into_iter().next() + && first_arg.to_string() == *r_name + { + *print_count += 1; + return true; + } + } + + // Any other reference to r → not allowed + // Check the token stream for the variable name + let ts_string = stmt.to_token_stream().to_string(); + if ts_string.contains(&format!("({r_name})")) + || ts_string.contains(&format!(" {r_name})")) + || ts_string.contains(&format!("(&mut {r_name})")) + || ts_string.contains(&format!("&{r_name}")) + || ts_string.contains(&format!("move {r_name}")) + || ts_string.contains(&format!(",{r_name},")) + { + return false; + } + true +} + +/// Build suggestion: `#[renderer]` → `#[renderer(buffer)]` +fn make_attr_suggestion(ast: &syn::ItemFn, source: &str) -> Option<LintSuggestion> { + let attr = ast.attrs.iter().find(|a| { + let name = a.path().to_token_stream().to_string(); + name.ends_with("renderer") + })?; + + let line_idx = attr.span().start().line.saturating_sub(1); + let line = source.lines().nth(line_idx)?; + + // Replace `renderer]` with `renderer(buffer)]` + // This handles both `#[renderer]` and `#[::mingling::macros::renderer]` + let line_str = line; + let replacement = line_str.replacen("renderer]", "renderer(buffer)]", 1); + + if replacement == line_str { + return None; + } + + Some(LintSuggestion { + source: line_str.to_string(), + line_start: line_idx + 1, + byte_range: 0..line_str.len(), + replacement, + }) +} + +/// Build suggestion: remove ` -> RenderResult` from function signature +fn make_return_type_suggestion(ast: &syn::ItemFn, source: &str) -> Option<LintSuggestion> { + let syn::ReturnType::Type(arrow, ret_type) = &ast.sig.output else { + return None; + }; + + let sig_line_idx = ast.sig.span().start().line.saturating_sub(1); + let line = source.lines().nth(sig_line_idx)?; + + // proc-macro2 column is 0-based byte offset from line start + let arrow_byte_col = arrow.span().start().column; + let ret_end_byte_col = ret_type.span().end().column; + + // Include the space before `->` + let range_start = if arrow_byte_col > 0 { + arrow_byte_col - 1 + } else { + arrow_byte_col + }; + + Some(LintSuggestion { + source: line.to_string(), + line_start: sig_line_idx + 1, + byte_range: range_start..ret_end_byte_col, + replacement: String::new(), + }) +} + +/// Build suggestion: remove `let mut r = RenderResult::...` line +fn make_let_removal_suggestion( + stmts: &[syn::Stmt], + let_idx: usize, + source: &str, +) -> Option<LintSuggestion> { + let stmt = &stmts[let_idx]; + let line_idx = stmt.span().start().line.saturating_sub(1); + let line = source.lines().nth(line_idx)?; + + Some(LintSuggestion { + source: line.to_string(), + line_start: line_idx + 1, + byte_range: 0..line.len(), + replacement: String::new(), + }) +} + +/// Build suggestions: fix `r_println!(r, ...)` → `r_println!(...)` for all r_xxx macros +fn make_macro_arg_suggestions( + stmts: &[syn::Stmt], + r_name: &str, + source: &str, +) -> Vec<LintSuggestion> { + let r_macros = ["r_println", "r_eprintln", "r_print", "r_eprint"]; + + stmts + .iter() + .filter_map(|stmt| { + let syn::Stmt::Macro(stmt_mac) = stmt else { + return None; + }; + + let macro_name = stmt_mac + .mac + .path + .segments + .last() + .map(|s| s.ident.to_string()) + .unwrap_or_default(); + + if !r_macros.contains(¯o_name.as_str()) { + return None; + } + + // Check that the first token is the r_name + let first_token = stmt_mac.mac.tokens.clone().into_iter().next()?; + if first_token.to_string() != *r_name { + return None; + } + + let line_idx = stmt.span().start().line.saturating_sub(1); + let line = source.lines().nth(line_idx)?; + + // Find pattern: macro_name!(r_name, ... + let macro_str = format!("{}!(", macro_name); + let macro_pos = line.find(¯o_str)?; + let after_open = macro_pos + macro_str.len(); + + // The first argument is `r_name` followed by `,` and possibly a space + // We need to find and remove `r_name, ` or `r_name,` + let first_arg = r_name; + if line[after_open..].starts_with(first_arg) { + // Find the end of the first argument (including `,` and any whitespace) + let arg_end = after_open + first_arg.len(); + if arg_end < line.len() { + let rest = &line[arg_end..]; + // Skip `,` and optional whitespace + let skip = if rest.starts_with(", ") { + 2 + } else if rest.starts_with(',') { + 1 + } else { + // Not followed by comma — not our pattern + return None; + }; + let range_end = arg_end + skip; + + Some(LintSuggestion { + source: line.to_string(), + line_start: line_idx + 1, + byte_range: after_open..range_end, + replacement: String::new(), + }) + } else { + None + } + } else { + None + } + }) + .collect() +} + +/// Build suggestion: remove the return `r` expression +fn make_return_removal_suggestion( + stmts: &[syn::Stmt], + r_name: &str, + source: &str, +) -> Option<LintSuggestion> { + let last = stmts.last()?; + + let is_r_return = match last { + // `r` (bare expression, no semicolon) or `r;` (with semicolon) + syn::Stmt::Expr(expr, _) => { + if let syn::Expr::Path(p) = expr { + p.path.is_ident(r_name) + } else if let syn::Expr::Return(ret) = expr { + ret.expr.as_ref().is_some_and(|e| { + if let syn::Expr::Path(p) = e.as_ref() { + p.path.is_ident(r_name) + } else { + false + } + }) + } else { + false + } + } + _ => false, + }; + + if !is_r_return { + return None; + } + + let line_idx = last.span().start().line.saturating_sub(1); + let line = source.lines().nth(line_idx)?; + + Some(LintSuggestion { + source: line.to_string(), + line_start: line_idx + 1, + byte_range: 0..line.len(), + replacement: String::new(), + }) +} + +#[cfg(test)] +mod lint_test { + use crate::{assert_detected, assert_not_detected}; + + #[test] + fn test_detected_render_result_new() { + assert_detected!(super::linter, syn::ItemFn => { + #[renderer] + fn render_somesthing(_: Prev) -> RenderResult { + let mut r = RenderResult::new(); + r_println!(r, ""); + r + } + }); + } + + #[test] + fn test_detected_render_result_default() { + assert_detected!(super::linter, syn::ItemFn => { + #[renderer] + fn render_somesthing(_: Prev) -> RenderResult { + let mut r = RenderResult::default(); + r_println!(r, ""); + r + } + }); + } + + #[test] + fn test_detected_render_result_from() { + assert_detected!(super::linter, syn::ItemFn => { + #[renderer] + fn render_somesthing(_: Prev) -> RenderResult { + let mut r = RenderResult::from("Hello".to_string()); + r_println!(r, ""); + r + } + }); + } + + #[test] + fn test_not_detected_with_other_function_call() { + assert_not_detected!(super::linter, syn::ItemFn => { + #[renderer] + fn render_somesthing(_: Prev) -> RenderResult { + let mut r = RenderResult::new(); + r_println!(r, ""); + other(&mut r); + r + } + }); + } + + #[test] + fn test_not_detected_without_renderer_attr() { + assert_not_detected!(super::linter, syn::ItemFn => { + fn render_somesthing(_: Prev) -> RenderResult { + let mut r = RenderResult::new(); + r_println!(r, ""); + r + } + }); + } + + #[test] + fn test_not_detected_with_buffer_attr() { + assert_not_detected!(super::linter, syn::ItemFn => { + #[::mingling::macros::renderer(::mingling::macros::buffer)] + fn render_somesthing(_: Prev) { + r_println!(""); + } + }); + } + + #[test] + fn test_not_detected_with_buffer_attr_fully_qualified() { + assert_not_detected!(super::linter, syn::ItemFn => { + #[::mingling::macros::renderer(::mingling::macros::buffer)] + fn render_somesthing(_: Prev) { + r_println!(""); + } + }); + } +} diff --git a/mingling_cli/src/main.rs b/mingling_cli/src/main.rs new file mode 100644 index 0000000..c822a12 --- /dev/null +++ b/mingling_cli/src/main.rs @@ -0,0 +1,29 @@ +use crate::{linter::MinglingLinterSetup, metadata::MinglingMetadataSetup}; +use mingling::{ + macros::gen_program, + setup::{ExitCodeSetup, picker::HelpFlagSetup}, +}; + +pub mod diagnostic; +pub mod errors; +pub mod linter; +pub mod lints; +pub mod message; +pub mod metadata; + +#[tokio::main] +async fn main() { + let mut program = ThisProgram::new(); + + // Setups + program.with_setup(HelpFlagSetup::default()); + program.with_setup(ExitCodeSetup::default()); + + program.with_setup(MinglingMetadataSetup); + program.with_setup(MinglingLinterSetup); + + // Exec + program.exec_and_exit().await; +} + +gen_program!(); diff --git a/mingling_cli/src/message.rs b/mingling_cli/src/message.rs new file mode 100644 index 0000000..3e50c30 --- /dev/null +++ b/mingling_cli/src/message.rs @@ -0,0 +1,10 @@ +use cargo_metadata::Message; +use mingling::macros::{buffer, group, r_println, renderer, renderify}; + +group!(Message); + +#[renderer(buffer, renderify)] +pub fn render_message(msg: Message) { + let r = serde_json::to_string(&msg)?; + r_println!("{}", r); +} diff --git a/mingling_cli/src/metadata.rs b/mingling_cli/src/metadata.rs new file mode 100644 index 0000000..2e7aeef --- /dev/null +++ b/mingling_cli/src/metadata.rs @@ -0,0 +1,24 @@ +use cargo_metadata::Metadata; +use mingling::{ + Program, + macros::{buffer, group, program_setup, r_println, renderer, renderify}, +}; + +use crate::metadata::{cmd_metadata::CMDMetadata, setup::CargoMetadataSetup}; + +pub mod cmd_metadata; +pub mod setup; + +#[program_setup] +pub fn mingling_metadata_setup(program: &mut Program<crate::ThisProgram>) { + program.with_setup(CargoMetadataSetup); + program.with_dispatcher(CMDMetadata); +} + +group!(Metadata); + +#[renderer(buffer, renderify)] +pub fn render_metadata(metadata: Metadata) { + let result = serde_json::to_string(&metadata)?; + r_println!("{}", result); +} diff --git a/mingling_cli/src/metadata/cmd_metadata.rs b/mingling_cli/src/metadata/cmd_metadata.rs new file mode 100644 index 0000000..18241a2 --- /dev/null +++ b/mingling_cli/src/metadata/cmd_metadata.rs @@ -0,0 +1,17 @@ +use cargo_metadata::Metadata; +use mingling::{ + LazyRes, + macros::{chain, dispatcher, pack}, +}; + +use crate::metadata::setup::ResMetadata; + +dispatcher!("metadata"); + +pack!(ResultMetadata = ResMetadata); + +#[chain] +pub fn handle_metadata(_: EntryMetadata, metadata: &mut LazyRes<ResMetadata>) -> Metadata { + let metadata = metadata.get_ref().clone(); + metadata.data().clone() +} diff --git a/mingling_cli/src/metadata/setup.rs b/mingling_cli/src/metadata/setup.rs new file mode 100644 index 0000000..198baf1 --- /dev/null +++ b/mingling_cli/src/metadata/setup.rs @@ -0,0 +1,136 @@ +use std::{env::current_dir, path::PathBuf}; + +use cargo_metadata::{CargoOpt, MetadataCommand}; +use mingling::{ + LazyInit, Program, + macros::program_setup, + picker::{IntoPicker, value::Flag}, + prelude::*, + res::ResCurrentDir, +}; + +/// Name of Cargo manifest file +pub const CARGO_TOML: &str = "Cargo.toml"; + +use cargo_metadata::Metadata; +use serde::Serialize; + +/// Resource holding parsed `cargo metadata` output. +/// +/// This is lazily initialized during program setup by calling `cargo metadata` +/// with the appropriate CLI flags (e.g., `--features`, `--all-features`, +/// `--no-default-features`, `--no-deps`, `--message-format`). +/// +/// Access the inner [`Metadata`] via [`ResMetadata::data()`], which panics if +/// called before initialization (guaranteed not to happen in normal usage). +#[derive(Default, Clone, Serialize)] +pub struct ResMetadata { + data: Option<Metadata>, +} + +/// Resource indicating whether the output format is JSON. +/// +/// Set to `true` when `--message-format json` (or similar) is passed. +/// Used by renderers to decide whether to serialize structs as JSON. +#[derive(Default, Clone, Serialize)] +pub struct ResUsingJson { + pub using: bool, +} + +impl ResMetadata { + /// Returns a reference to the parsed `cargo metadata`. + /// + /// # Panics + /// + /// This function does **not** panic in practice, because `ResMetadata` is + /// always initialized via [`LazyInit`] inside the program setup, and the + /// initialization either succeeds (setting `data` to `Some(...)`) or fails + /// by propagating the error from `cmd.exec().unwrap()`. Therefore, by the + /// time this getter is called, `self.data` is guaranteed to be `Some`. + pub fn data(&self) -> &Metadata { + self.data.as_ref().unwrap() + } +} + +#[program_setup] +pub fn cargo_metadata_setup(program: &mut Program<crate::ThisProgram>) { + let args = program.get_args().to_vec(); + + let ( + feature_args, + manifest_path, + message_format, + enable_all_features, + no_default_features, + no_deps, + ) = args + .pick(&arg![features: Vec<String>]) + .pick(&arg![manifest_path: Option<String>]) + .pick_or(&arg![message_format: String], || "disable".to_string()) + .pick(&arg![all_features: Flag]) + .pick(&arg![no_default_features: Flag]) + .pick(&arg![no_deps: Flag]) + .unwrap(); + + // Is Using Json + program.with_resource(ResUsingJson { + using: message_format.contains("json"), + }); + + // Current Dir + let current_dir = current_dir().unwrap(); + + // Leak `feature_args` into a static slice so it can be captured by the metadata closure. + // Since the process does not loop, this memory leak is harmless. + let feature_args_leaked: &[String] = Box::leak(feature_args.into_boxed_slice()); + let manifest_path_clone = manifest_path.clone(); + let current_dir_clone = current_dir.clone(); + + program.with_resource(ResMetadata::lazy_init(move || { + // Paths + let metadata_path = match manifest_path_clone { + Some(ref path_str) => PathBuf::from(path_str), + None => find_manifest().unwrap(), + }; + + // Cargo Metadata - bind to a longer-lived variable + let mut cmd = MetadataCommand::new(); + cmd.manifest_path(metadata_path) + .current_dir(¤t_dir_clone); + + if *enable_all_features { + cmd.features(CargoOpt::AllFeatures); + } + + if *no_default_features { + cmd.features(CargoOpt::NoDefaultFeatures); + } + + if *no_deps { + cmd.no_deps(); + } + + cmd.features(CargoOpt::SomeFeatures(feature_args_leaked.to_vec())); + + ResMetadata { + data: Some(cmd.exec().unwrap()), + } + })); + + // Current Dir + program.with_resource(ResCurrentDir::from(current_dir)); +} + +/// Find `Cargo.toml` by searching upward from `current_dir`. +fn find_manifest() -> Option<PathBuf> { + let mut dir = current_dir().ok()?; + loop { + let candidate = dir.join(CARGO_TOML); + if candidate.is_file() { + return Some(candidate); + } + if !dir.pop() { + return None; + } + } +} diff --git a/mingling_cli/tmpls/lints.tmpl b/mingling_cli/tmpls/lints.tmpl new file mode 100644 index 0000000..590828a --- /dev/null +++ b/mingling_cli/tmpls/lints.tmpl @@ -0,0 +1,78 @@ +#![allow(unused)] +use crate::linter::mlint_report::{MlintLevel, MlintReport}; + +>>>>>>>>>> impls; +>>>>>>>>>> file_lints; + +/// Run all registered lints on a parsed file with its source text. +pub fn run_all_lints(file: &syn::File, source: &str) -> Vec<MlintReport> { + use crate::linter::mlint_attr::{get_mlint_override, MlintLevelOverride}; + + // File-level lints (check types, modules, etc.) + let mut reports: Vec<MlintReport> = vec![]; +>>>>>>>>>> file_calls; + + // Item-level lints (check each function) + for item in &file.items { + if let syn::Item::Fn(f) = item { +>>>>>>>>>> calls; + } + } + + // Apply file-level #![mlint(allow/warn/deny(...))] overrides + for r in &mut reports { + if let Some(override_level) = get_mlint_override(&file.attrs, &r.lint_code) { + match override_level { + MlintLevelOverride::Allow => r.level = MlintLevel::Help, + MlintLevelOverride::Deny => r.level = MlintLevel::Error, + MlintLevelOverride::Warn => { + if r.level != MlintLevel::Error { r.level = MlintLevel::Warning; } + } + } + } + } + reports.retain(|r| r.level != MlintLevel::Help); + reports +} + +#[macro_export] +macro_rules! assert_detected { + ($linter:expr, $ast_type:ty => { $($code:tt)* }) => { + let source = stringify!($($code)*); + let ast: $ast_type = syn::parse_str(&source).unwrap(); + assert!(!$linter(ast, &source).is_empty()); + }; +} + +#[macro_export] +macro_rules! assert_not_detected { + ($linter:expr, $ast_type:ty => { $($code:tt)* }) => { + let source = stringify!($($code)*); + let ast: $ast_type = syn::parse_str(&source).unwrap(); + assert!($linter(ast, &source).is_empty()); + }; +} + +@@@ >>> file_calls + reports.extend(<<<name>>>::check_file(file, source)); +@@@ <<< + +@@@ >>> file_lints +pub use <<<name>>>::check_file as <<<name>>>_file; +@@@ <<< + +@@@ >>> impls +mod <<<mod_name>>>; +pub use <<<mod_name>>>::linter as <<<mod_name>>>; +@@@ <<< + +@@@ >>> calls + let skip = get_mlint_override(&f.attrs, "<<<name>>>") == Some(MlintLevelOverride::Allow); + if !skip { + let mut rs = <<<name>>>::linter(f.clone(), source); + if get_mlint_override(&f.attrs, "<<<name>>>") == Some(MlintLevelOverride::Deny) { + for r in &mut rs { r.level = MlintLevel::Error; } + } + reports.extend(rs); + } +@@@ <<< diff --git a/mingling_core/src/any.rs b/mingling_core/src/any.rs index e6b7406..3e8fdf0 100644 --- a/mingling_core/src/any.rs +++ b/mingling_core/src/any.rs @@ -17,8 +17,19 @@ pub mod group; #[derive(Debug)] pub struct AnyOutput<G> { pub(crate) inner: Box<dyn std::any::Any + Send + 'static>, - pub type_id: std::any::TypeId, - pub member_id: G, + + /// 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(crate) 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(crate) member_id: G, } impl<G> AnyOutput<G> { @@ -34,6 +45,52 @@ impl<G> AnyOutput<G> { } } + /// Create an `AnyOutput` from a raw value with a manually specified member_id. + /// + /// This function bypasses the [`Grouped`] trait, meaning the `member_id` you provide + /// does **not** have to match the actual concrete type `T`. The scheduler uses + /// `member_id` to determine which enum variant the output belongs to, and later + /// attempts to restore the value to the concrete type `T` based on that variant. + /// + /// # Safety + /// + /// - The caller must ensure that `member_id` correctly corresponds to the concrete + /// type `T` according to the scheduling logic. If `member_id` does not match, + /// calling [`restore`](Self::restore) or [`downcast`](Self::downcast) with the + /// type associated with `member_id` will cause **undefined behavior**. + /// - This safety contract is the caller's responsibility; the compiler cannot + /// enforce the correspondence between `member_id` and the stored type. + pub unsafe fn new_bare<T>(value: T, member_id: G) -> Self + where + T: Send + 'static, + { + Self { + inner: Box::new(value), + type_id: std::any::TypeId::of::<T>(), + member_id, + } + } + + /// Get the [`TypeId`] of the concrete type stored in `inner`. + /// + /// The `TypeId` is set during construction (via [`AnyOutput::new`] or [`AnyOutput::new_bare`]) + /// and is used for subsequent downcasting and type checking. + pub fn type_id(&self) -> std::any::TypeId { + self.type_id + } + + /// Get the [`member_id`] of the concrete type stored in `inner`. + /// + /// [`member_id`] is set during construction (via [`AnyOutput::new`] or [`AnyOutput::new_bare`]) + /// and identifies which variant of the output enum this value corresponds to. + /// The scheduler uses this value to dispatch the output to the correct next step. + pub fn member_id(&self) -> G + where + G: Copy, + { + self.member_id + } + /// Attempt to downcast the `AnyOutput` to a concrete type. /// /// # Errors @@ -106,7 +163,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 +176,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, } @@ -175,7 +236,17 @@ mod tests { value: i32, } - impl Grouped<MockGroup> for AlphaData { + /// # Safety + /// + /// This implementation is only for testing purposes to satisfy trait bounds. + /// Since this code only constructs `AnyOutput` and calls methods like + /// `downcast`, `is`, `restore`, `route_chain`, and `route_renderer` — + /// none of which involve `ProgramCollect::do_chain` or + /// `ProgramCollect::render` — the type/member_id correspondence is + /// never exploited in an unsafe way here. + /// The caller must ensure that the associated `member_id` correctly + /// corresponds to the type's role in the group. + unsafe impl Grouped<MockGroup> for AlphaData { fn member_id() -> MockGroup { MockGroup::Alpha } @@ -187,7 +258,17 @@ mod tests { name: String, } - impl Grouped<MockGroup> for BetaData { + /// # Safety + /// + /// This implementation is only for testing purposes to satisfy trait bounds. + /// Since this code only constructs `AnyOutput` and calls methods like + /// `downcast`, `is`, `restore`, `route_chain`, and `route_renderer` — + /// none of which involve `ProgramCollect::do_chain` or + /// `ProgramCollect::render` — the type/member_id correspondence is + /// never exploited in an unsafe way here. + /// The caller must ensure that the associated `member_id` correctly + /// corresponds to the type's role in the group. + unsafe impl Grouped<MockGroup> for BetaData { fn member_id() -> MockGroup { MockGroup::Beta } @@ -198,7 +279,17 @@ mod tests { #[cfg_attr(feature = "structural_renderer", derive(serde::Serialize))] struct GammaData; - impl Grouped<MockGroup> for GammaData { + /// # Safety + /// + /// This implementation is only for testing purposes to satisfy trait bounds. + /// Since this code only constructs `AnyOutput` and calls methods like + /// `downcast`, `is`, `restore`, `route_chain`, and `route_renderer` — + /// none of which involve `ProgramCollect::do_chain` or + /// `ProgramCollect::render` — the type/member_id correspondence is + /// never exploited in an unsafe way here. + /// The caller must ensure that the associated `member_id` correctly + /// corresponds to the type's role in the group. + unsafe impl Grouped<MockGroup> for GammaData { fn member_id() -> MockGroup { MockGroup::Gamma } @@ -354,7 +445,17 @@ mod tests { x: i32, } - impl Grouped<MockGroup> for SerData { + /// SAFETY: + /// + /// This implementation is only for testing purposes to satisfy trait bounds. + /// Since this code only constructs `AnyOutput` and calls methods like + /// `downcast`, `is`, `restore`, `route_chain`, and `route_renderer` — + /// none of which involve `ProgramCollect::do_chain` or + /// `ProgramCollect::render` — the type/member_id correspondence is + /// never exploited in an unsafe way here. + /// The caller must ensure that the associated `member_id` correctly + /// corresponds to the type's role in the group. + unsafe impl Grouped<MockGroup> for SerData { fn member_id() -> MockGroup { MockGroup::Gamma } @@ -381,13 +482,33 @@ mod tests { b: String, } - impl Grouped<MockGroup> for SerA { + /// SAFETY: + /// + /// This implementation is only for testing purposes to satisfy trait bounds. + /// Since this code only constructs `AnyOutput` and calls methods like + /// `downcast`, `is`, `restore`, `route_chain`, and `route_renderer` — + /// none of which involve `ProgramCollect::do_chain` or + /// `ProgramCollect::render` — the type/member_id correspondence is + /// never exploited in an unsafe way here. + /// The caller must ensure that the associated `member_id` correctly + /// corresponds to the type's role in the group. + unsafe impl Grouped<MockGroup> for SerA { fn member_id() -> MockGroup { MockGroup::Alpha } } - impl Grouped<MockGroup> for SerB { + /// SAFETY: + /// + /// This implementation is only for testing purposes to satisfy trait bounds. + /// Since this code only constructs `AnyOutput` and calls methods like + /// `downcast`, `is`, `restore`, `route_chain`, and `route_renderer` — + /// none of which involve `ProgramCollect::do_chain` or + /// `ProgramCollect::render` — the type/member_id correspondence is + /// never exploited in an unsafe way here. + /// The caller must ensure that the associated `member_id` correctly + /// corresponds to the type's role in the group. + unsafe impl Grouped<MockGroup> for SerB { fn member_id() -> MockGroup { MockGroup::Beta } diff --git a/mingling_core/src/any/group.rs b/mingling_core/src/any/group.rs index 90ef9fc..5e5e347 100644 --- a/mingling_core/src/any/group.rs +++ b/mingling_core/src/any/group.rs @@ -2,35 +2,19 @@ use crate::{AnyOutput, ChainProcess, ProgramCollect, Routable}; /// Used to mark a type with a unique enum ID, assisting dynamic dispatch /// -/// **Note:** Unlike earlier versions, `Grouped` no longer requires `Serialize` -/// even when the `structural_renderer` feature is enabled. Structured output is -/// controlled separately via the \[`StructuralData`\] trait. -pub trait Grouped<Group> +/// # Safety +/// +/// The returned `Group` value is an enum variant created by `register_type!` when +/// registering the type's ID. Whether the variant matches correctly is guaranteed +/// by `Grouped derive` or macros like `pack!`. If implemented manually, and the +/// type name written in `member_id()` does not match the actually registered type, +/// dispatching to that type will result in **100% undefined behavior**. +pub unsafe trait Grouped<Group> where Self: Sized + 'static, { /// 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 +23,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..cb0987d 100644 --- a/mingling_core/src/asset/dispatcher.rs +++ b/mingling_core/src/asset/dispatcher.rs @@ -32,22 +32,48 @@ where C: ProgramCollect<Enum = C>, { /// Adds a dispatcher to the program. - #[cfg(not(feature = "dispatch_tree"))] - pub fn with_dispatcher<Disp>(&mut self, dispatcher: Disp) + #[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) -> &mut Self 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; + } + self } /// Add some dispatchers to the program. - #[cfg(not(feature = "dispatch_tree"))] - pub fn with_dispatchers<D>(&mut self, dispatchers: D) + #[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) -> &mut Self 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; + } + self } } diff --git a/mingling_core/src/asset/global_resource.rs b/mingling_core/src/asset/global_resource.rs index 29e1136..a610378 100644 --- a/mingling_core/src/asset/global_resource.rs +++ b/mingling_core/src/asset/global_resource.rs @@ -13,10 +13,14 @@ where C: ProgramCollect<Enum = C>, { /// Insert a resource of the given type, cloning the provided value into the store - pub fn with_resource<Res: 'static + Send + Sync + ResourceMarker>(&mut self, res: Res) { + pub fn with_resource<Res: 'static + Send + Sync + ResourceMarker>( + &mut self, + res: Res, + ) -> &mut Self { if let Ok(mut guard) = self.resources.lock() { guard.insert(TypeId::of::<Res>(), Box::new(Arc::new(res))); } + self } /// Modify a resource by type, applying a closure to the resource if present diff --git a/mingling_core/src/lib.rs b/mingling_core/src/lib.rs index 4996b19..31476c9 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; } @@ -84,8 +90,14 @@ pub mod setup { /// Private API — not intended for direct use. #[doc(hidden)] pub mod __private { + use crate::ProgramCollect; + /// Sealed trait for `StructuralData` — only implementable via derive macro. - pub trait StructuralDataSealed {} + pub trait StructuralDataSealed<C> + where + C: ProgramCollect<Enum = C>, + { + } /// Re-export so the derive macro can reference the trait without /// conflicting with the derive macro name at `::mingling::StructuralData`. 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/program/collection/mock.rs b/mingling_core/src/program/collection/mock.rs index 5847f10..dbe4789 100644 --- a/mingling_core/src/program/collection/mock.rs +++ b/mingling_core/src/program/collection/mock.rs @@ -23,9 +23,12 @@ pub enum MockProgramCollect { Bar, } -impl Grouped<MockProgramCollect> for MockProgramCollect { +/// SAFETY: This is a mock type used only for temporary testing. +/// It will never actually enter the macro system. +/// The internal `panic!` ensures that `member_id` will never be executed. +unsafe impl Grouped<MockProgramCollect> for MockProgramCollect { fn member_id() -> MockProgramCollect { - MockProgramCollect::Foo + panic!("Attempting to read an unsafe enum type"); } } diff --git a/mingling_core/src/program/hook.rs b/mingling_core/src/program/hook.rs index db1691b..7d94a21 100644 --- a/mingling_core/src/program/hook.rs +++ b/mingling_core/src/program/hook.rs @@ -144,8 +144,9 @@ where { /// Adds a typed hook to the program. The hook will be called at the appropriate /// lifecycle events. - pub fn with_hook(&mut self, hook: ProgramHook<C>) { + pub fn with_hook(&mut self, hook: ProgramHook<C>) -> &mut Self { self.hooks.push(hook); + self } pub(crate) fn run_hook_on_begin(&self, info: HookBeginInfo) { @@ -694,7 +695,17 @@ mod tests { } } - impl Grouped<MockHookEnum> for MockHookEnum { + /// SAFETY: + /// + /// This implementation is only for testing purposes to satisfy trait bounds. + /// Since this code only constructs `AnyOutput` and calls methods like + /// `downcast`, `is`, `restore`, `route_chain`, and `route_renderer` — + /// none of which involve `ProgramCollect::do_chain` or + /// `ProgramCollect::render` — the type/member_id correspondence is + /// never exploited in an unsafe way here. + /// The caller must ensure that the associated `member_id` correctly + /// corresponds to the type's role in the group. + unsafe impl Grouped<MockHookEnum> for MockHookEnum { fn member_id() -> MockHookEnum { MockHookEnum::A } diff --git a/mingling_core/src/program/once_exec.rs b/mingling_core/src/program/once_exec.rs index 9d6f1e4..a846d04 100644 --- a/mingling_core/src/program/once_exec.rs +++ b/mingling_core/src/program/once_exec.rs @@ -79,23 +79,12 @@ where }; // Read exit code - let exit_code = result.exit_code; - // Render result - if stdout_setting.render_output && !result.is_empty() { - print!("{result}"); - - if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) - && stdout_setting.error_output - { - eprintln!("{e}"); - 1 - } else { - exit_code - } - } else { - exit_code + if stdout_setting.render_output { + result.std_print(); } + + result.exit_code } /// Run the command line program, then exit @@ -217,23 +206,12 @@ where }; // Read exit code - let exit_code = result.exit_code; - // Render result - if stdout_setting.render_output && !result.is_empty() { - print!("{result}"); - - if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) - && stdout_setting.error_output - { - eprintln!("{e}"); - 1 - } else { - exit_code - } - } else { - exit_code + if stdout_setting.render_output { + result.std_print(); } + + result.exit_code } /// Run the command line program, then exit diff --git a/mingling_core/src/program/setup.rs b/mingling_core/src/program/setup.rs index 838c29a..a8ff114 100644 --- a/mingling_core/src/program/setup.rs +++ b/mingling_core/src/program/setup.rs @@ -29,8 +29,9 @@ where C: ProgramCollect<Enum = C>, { /// Load and execute init logic - pub fn with_setup<S: ProgramSetup<C> + 'static>(&mut self, setup: S) { + pub fn with_setup<S: ProgramSetup<C> + 'static>(&mut self, setup: S) -> &mut Self { S::setup(setup, self); + self } } diff --git a/mingling_core/src/renderer/render_result.rs b/mingling_core/src/renderer/render_result.rs index e57a5b9..c38522a 100644 --- a/mingling_core/src/renderer/render_result.rs +++ b/mingling_core/src/renderer/render_result.rs @@ -1,22 +1,67 @@ use std::{ fmt::{Display, Formatter}, io::Write, - ops::Deref, }; +use crate::RenderResultMode::{Stderr, Stdout}; + /// Render result, containing the rendered text content. #[derive(Default, Debug, PartialEq)] pub struct RenderResult { - render_text: String, + /// Whether the output should be written immediately. + /// + /// When set to `true`, rendered content will be flushed to stdout/stderr + /// in real time while also being collected in the render buffer. + immediate_output: bool, + + /// The buffered render output, stored as a list of (text, mode) pairs. + /// + /// Each entry contains a rendered string together with a `RenderResultMode` + /// indicating whether it should be output to stdout or stderr. + render_buffer: Vec<(String, RenderResultMode)>, + + /// 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, } +/// Enum representing the output mode for render results. +/// +/// This determines whether the rendered content should be directed to standard +/// output or standard error. +/// +/// # Variants +/// +/// * `Stdout` - Output will be written to standard output (stdout). +/// * `Stderr` - Output will be written to standard error (stderr). +#[repr(u8)] +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +pub enum RenderResultMode { + /// Standard output (stdout). + #[default] + Stdout = 0, + + /// Standard error (stderr). + Stderr = 1, +} + +impl<F> From<F> for RenderResult +where + F: FnOnce() -> RenderResult, +{ + fn from(value: F) -> Self { + value() + } +} + impl Write for RenderResult { fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { let s = std::str::from_utf8(buf).map_err(|_| { std::io::Error::new(std::io::ErrorKind::InvalidInput, "not valid UTF-8") })?; - self.render_text.push_str(s); + self.append_to_buffer(s, Stdout); Ok(buf.len()) } @@ -27,15 +72,7 @@ impl Write for RenderResult { impl Display for RenderResult { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "{}", self.render_text.trim()) - } -} - -impl Deref for RenderResult { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.render_text + write!(f, "{}", render_result_to_string(self).trim()) } } @@ -64,40 +101,31 @@ impl_from_int!(i32, i16, i8, u32, u16, u8, usize); impl From<&String> for RenderResult { fn from(value: &String) -> Self { - RenderResult { - render_text: value.clone(), - exit_code: 0, - } + string_to_render_result(value, Stdout) } } impl From<String> for RenderResult { fn from(value: String) -> Self { - RenderResult { - render_text: value, - exit_code: 0, - } + string_to_render_result(value, Stdout) } } impl From<&str> for RenderResult { fn from(value: &str) -> Self { - RenderResult { - render_text: value.to_string(), - exit_code: 0, - } + string_to_render_result(value, Stdout) } } impl From<RenderResult> for String { fn from(result: RenderResult) -> Self { - result.render_text + render_result_to_string(&result) } } impl From<&RenderResult> for String { fn from(result: &RenderResult) -> Self { - result.render_text.clone() + render_result_to_string(result) } } @@ -119,21 +147,139 @@ impl RenderResult { Self::default() } + /// Marks the render result for immediate output, bypassing any buffering or + /// deferred rendering. + /// + /// When set, the rendered content will be both collected in the result and + /// immediately flushed to stdout/stderr in real time, rather than being + /// deferred for later display. + /// + /// # Examples + /// + /// ``` + /// use mingling_core::RenderResult; + /// + /// let mut result = RenderResult::default(); + /// result.immediate_output(); + /// ``` + pub fn immediate_output(&mut self) -> &mut Self { + self.immediate_output = true; + self + } + + /// Appends the given text and mode to the render buffer. + /// + /// Unlike `print` and `println` which only store plain text in a single string, + /// this method stores the text along with a `RenderResultMode` that indicates + /// whether the output should be directed to stdout or stderr. This allows for + /// more fine-grained control over output routing when the buffer is later flushed. + /// + /// # Arguments + /// + /// * `text` - The text content to append to the buffer. + /// * `mode` - The output mode (`Stdout` or `Stderr`) indicating where the text + /// should be directed. + /// + /// # Examples + /// + /// ``` + /// use mingling_core::{RenderResult, RenderResultMode}; + /// + /// let mut result = RenderResult::default(); + /// result.append_to_buffer("Hello", RenderResultMode::Stdout); + /// result.append_to_buffer("Error message", RenderResultMode::Stderr); + /// ``` + pub fn append_to_buffer(&mut self, text: impl Into<String>, mode: RenderResultMode) { + self.render_buffer.push((text.into(), mode)); + } + + /// Appends the given text followed by a newline, along with the mode, to the render buffer. + /// + /// This is a convenience method that calls `append_to_buffer` for the text and then + /// appends a newline with the same mode. + /// + /// # Arguments + /// + /// * `text` - The text content to append to the buffer. + /// * `mode` - The output mode (`Stdout` or `Stderr`) indicating where the text + /// should be directed. + /// + /// # Examples + /// + /// ``` + /// use mingling_core::{RenderResult, RenderResultMode}; + /// + /// let mut result = RenderResult::default(); + /// result.append_line_to_buffer("Hello", RenderResultMode::Stdout); + /// result.append_line_to_buffer("Warning", RenderResultMode::Stderr); + /// ``` + pub fn append_line_to_buffer(&mut self, text: impl Into<String>, mode: RenderResultMode) { + self.append_to_buffer(text, mode); + self.append_to_buffer("\n", mode); + } + + /// Appends the contents of another `RenderResult` to this one. + /// + /// If this `RenderResult` has `immediate_output` enabled but the other does not, + /// the other's content will be immediately flushed to the appropriate output stream + /// (stdout/stderr) while also being appended to the render buffer. + /// + /// The `exit_code` of the other result is **not** transferred — only the buffered + /// content and the `immediate_output` flag of the other result are merged. + /// + /// # Arguments + /// + /// * `other` - The `RenderResult` whose contents should be appended. + /// + /// # Examples + /// + /// ``` + /// use mingling_core::{RenderResult, RenderResultMode}; + /// + /// let mut dest = RenderResult::default(); + /// let mut src = RenderResult::default(); + /// + /// src.append_to_buffer("Hello", RenderResultMode::Stdout); + /// src.append_to_buffer(" Error", RenderResultMode::Stderr); + /// + /// dest.append_other(src); + /// assert_eq!(dest.to_string(), "Hello Error"); + /// ``` + pub fn append_other(&mut self, other: impl Into<RenderResult>) { + let other = other.into(); + + // If self has immediate output enabled, but the input does not, the input needs immediate output. + let immediate_output = !other.immediate_output && self.immediate_output; + + for i in other.render_buffer { + if immediate_output { + match &i.1 { + Stdout => print!("{}", i.0), + Stderr => eprint!("{}", i.0), + } + } + self.render_buffer.push(i); + } + } + /// Appends the given text to the rendered content. /// /// # Examples /// /// ``` /// use mingling_core::RenderResult; - /// use std::ops::Deref; /// /// let mut result = RenderResult::default(); /// result.print("Hello"); /// result.print(", world!"); - /// assert_eq!(result.deref(), "Hello, world!"); + /// assert_eq!(result.to_string(), "Hello, world!"); /// ``` - pub fn print(&mut self, text: &str) { - self.render_text.push_str(text); + pub fn print(&mut self, text: impl Into<String>) { + let text = text.into(); + if self.immediate_output { + print!("{}", text) + } + self.append_to_buffer(text, Stdout); } /// Appends the given text followed by a newline to the rendered content. @@ -142,16 +288,58 @@ impl RenderResult { /// /// ``` /// use mingling_core::RenderResult; - /// use std::ops::Deref; /// /// let mut result = RenderResult::default(); /// result.println("First line"); /// result.println("Second line"); - /// assert_eq!(result.deref(), "First line\nSecond line\n"); + /// assert_eq!(result.to_string(), "First line\nSecond line"); + /// ``` + pub fn println(&mut self, text: impl Into<String>) { + let text = text.into(); + if self.immediate_output { + println!("{}", text) + } + self.append_line_to_buffer(text, Stdout); + } + + /// Appends the given text to the rendered content, marking it for stderr output. + /// + /// # Examples + /// + /// ``` + /// use mingling_core::RenderResult; + /// + /// let mut result = RenderResult::default(); + /// result.eprint("Hello"); + /// result.eprint(", world!"); + /// assert_eq!(result.to_string(), "Hello, world!"); /// ``` - pub fn println(&mut self, text: &str) { - self.render_text.push_str(text); - self.render_text.push('\n'); + pub fn eprint(&mut self, text: impl Into<String>) { + let text = text.into(); + if self.immediate_output { + eprint!("{}", text) + } + self.append_to_buffer(text, Stderr); + } + + /// Appends the given text followed by a newline to the rendered content, marking it for stderr output. + /// + /// # Examples + /// + /// ``` + /// use mingling_core::RenderResult; + /// + /// let mut result = RenderResult::default(); + /// result.eprintln("First line"); + /// result.eprintln("Second line"); + /// assert_eq!(result.to_string(), "First line\nSecond line"); + /// ``` + pub fn eprintln(&mut self, text: impl Into<String>) { + let text = text.into(); + if self.immediate_output { + println!("{}", text) + } + self.append_line_to_buffer(text, Stderr); } /// Clears all rendered content. @@ -169,7 +357,148 @@ impl RenderResult { /// assert!(result.is_empty()); /// ``` pub fn clear(&mut self) { - self.render_text.clear(); + self.render_buffer.clear(); + } + + /// Outputs all buffered content to stdout and stderr according to their respective modes. + /// + /// Iterates through the render buffer and prints each buffered string to the appropriate + /// output stream — stdout for `Stdout` entries and stderr for `Stderr` entries. + /// + /// This method is typically used to flush the buffered output at the end of rendering, + /// ensuring that all output is displayed in the correct order and to the correct stream. + /// + /// # Examples + /// + /// ``` + /// use mingling_core::{RenderResult, RenderResultMode}; + /// + /// let mut result = RenderResult::default(); + /// result.append_to_buffer("Hello", RenderResultMode::Stdout); + /// result.append_to_buffer("Error", RenderResultMode::Stderr); + /// result.std_print(); // prints "Hello" to stdout and "Error" to stderr + /// ``` + pub fn std_print(&self) { + for (content, mode) in self.render_buffer.iter() { + match mode { + Stdout => print!("{}", content), + Stderr => eprint!("{}", content), + } + } + } + + /// Returns the total number of characters (in terms of `char` count) in the buffered render output. + /// + /// This counts the length across all buffered entries, regardless of whether they are + /// destined for stdout or stderr. + /// + /// # Examples + /// + /// ``` + /// use mingling_core::RenderResult; + /// + /// let mut result = RenderResult::default(); + /// result.print("Hello"); + /// result.print(", 世界"); + /// assert_eq!(result.len(), 9); // "Hello, 世界" has 9 chars + /// ``` + pub fn len(&self) -> usize { + self.render_buffer + .iter() + .map(|(s, _)| s.chars().count()) + .sum() + } + + /// Returns `true` if the buffered render output contains no characters. + /// + /// # Examples + /// + /// ``` + /// use mingling_core::RenderResult; + /// + /// let mut result = RenderResult::default(); + /// assert!(result.is_empty()); + /// result.print("Hello"); + /// assert!(!result.is_empty()); + /// ``` + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Trims leading and trailing whitespace from the buffered render output. + /// + /// This method processes the render buffer as follows: + /// - If the buffer is empty, it returns `self` unchanged. + /// - If there is only one entry, whitespace is trimmed from both the start and end of that + /// single entry. + /// - If there are multiple entries, whitespace is trimmed from the start of the first entry + /// and the end of the last entry. + /// + /// Whitespace in the middle entries is preserved. This is useful for cleaning up output + /// without removing intentional spacing between separately buffered segments. + /// + /// # Returns + /// + /// A new `RenderResult` with the same `immediate_output` flag and `exit_code`, but with + /// trimmed text content. + /// + /// # Examples + /// + /// ``` + /// use mingling_core::RenderResult; + /// + /// let mut result = RenderResult::default(); + /// result.print(" Hello, world! "); + /// let trimmed = result.trim_buffer(); + /// assert_eq!(trimmed.to_string().trim(), "Hello, world!"); + /// ``` + pub fn trim_buffer(self) -> RenderResult { + if self.render_buffer.is_empty() { + return self; + } + + let mut buffer = self.render_buffer; + if buffer.len() == 1 { + // Only one entry: trim both start and end of this single entry + let (text, mode) = buffer.remove(0); + buffer.push((text.trim().to_string(), mode)); + } else { + // Multiple entries: trim start of first, trim end of last + let first_len = buffer.len(); + + // Trim start of first entry + let (first_text, first_mode) = buffer.remove(0); + let trimmed_first = first_text.trim_start().to_string(); + buffer.insert(0, (trimmed_first, first_mode)); + + // Trim end of last entry + let (last_text, last_mode) = buffer.remove(first_len - 1); + let trimmed_last = last_text.trim_end().to_string(); + buffer.push((trimmed_last, last_mode)); + } + + RenderResult { + render_buffer: buffer, + immediate_output: self.immediate_output, + exit_code: self.exit_code, + } + } +} + +#[inline(always)] +fn render_result_to_string(result: &RenderResult) -> String { + let mut buffer = String::new(); + for item in result.render_buffer.iter() { + buffer += &item.0; + } + buffer +} + +#[inline(always)] +fn string_to_render_result(string: impl Into<String>, mode: RenderResultMode) -> RenderResult { + RenderResult { + render_buffer: vec![(string.into(), mode)], + ..Default::default() } } @@ -186,20 +515,6 @@ mod tests { } #[test] - fn print_appends_text() { - let mut result = RenderResult::default(); - result.print("Hello"); - assert_eq!(result.deref(), "Hello"); - } - - #[test] - fn println_appends_text_with_newline() { - let mut result = RenderResult::default(); - result.println("Hello"); - assert_eq!(result.deref(), "Hello\n"); - } - - #[test] fn clear_empties_content() { let mut result = RenderResult::default(); result.print("something"); @@ -217,14 +532,6 @@ mod tests { } #[test] - fn write_appends_utf8_bytes() { - let mut result = RenderResult::default(); - let n = IoWrite::write(&mut result, b"hello").unwrap(); - assert_eq!(n, 5); - assert_eq!(result.deref(), "hello"); - } - - #[test] fn write_with_invalid_utf8_returns_error() { let mut result = RenderResult::default(); let err = IoWrite::write(&mut result, &[0xff, 0xfe]).unwrap_err(); @@ -236,16 +543,7 @@ mod tests { let mut result = RenderResult::default(); result.print(" hello world \n"); let formatted = format!("{}", result); - assert_eq!(formatted, "hello world\n"); - } - - #[test] - fn deref_exposes_inner_text_as_str() { - let mut result = RenderResult::default(); - result.print("test"); - - let s: &str = &result; - assert_eq!(s, "test"); + assert_eq!(formatted, "hello world"); } #[test] @@ -265,4 +563,72 @@ mod tests { // original is still usable assert!(!result.is_empty()); } + + #[test] + fn trim_empty_buffer_returns_self() { + let result = RenderResult::default(); + let trimmed = result.trim_buffer(); + assert!(trimmed.is_empty()); + assert_eq!(trimmed.exit_code, 0); + } + + #[test] + fn trim_single_entry_trims_both_ends() { + let mut result = RenderResult::default(); + result.print(" Hello, world! "); + let trimmed = result.trim_buffer(); + assert_eq!(trimmed.to_string(), "Hello, world!"); + } + + #[test] + fn trim_single_entry_nothing_to_trim() { + let mut result = RenderResult::default(); + result.print("Hello"); + let trimmed = result.trim_buffer(); + assert_eq!(trimmed.to_string(), "Hello"); + } + + #[test] + fn trim_multiple_entries_trims_first_start_and_last_end() { + let mut result = RenderResult::default(); + result.print(" Hello"); + result.print(" World "); + result.print("! "); + let trimmed = result.trim_buffer(); + // first entry trim_start: "Hello" + // middle entry unchanged: " World " + // last entry trim_end: "!" + assert_eq!(trimmed.to_string(), "Hello World !"); + } + + #[test] + fn trim_multiple_entries_only_whitespace_first_entry() { + let mut result = RenderResult::default(); + result.print(" "); + result.print("Hello"); + result.print(" "); + let trimmed = result.trim_buffer(); + // first entry trim_start: "" + // middle entry unchanged: "Hello" + // last entry trim_end: "" + assert_eq!(trimmed.to_string(), "Hello"); + } + + #[test] + fn trim_preserves_exit_code() { + let mut result = RenderResult::new(); + result.exit_code = 42; + result.print(" test "); + let trimmed = result.trim_buffer(); + assert_eq!(trimmed.exit_code, 42); + } + + #[test] + fn trim_preserves_stderr_mode() { + let mut result = RenderResult::default(); + result.eprint(" error "); + let trimmed = result.trim_buffer(); + assert_eq!(trimmed.render_buffer[0].1, RenderResultMode::Stderr); + assert_eq!(trimmed.to_string(), "error"); + } } diff --git a/mingling_core/src/renderer/structural.rs b/mingling_core/src/renderer/structural.rs index 30255aa..0449e72 100644 --- a/mingling_core/src/renderer/structural.rs +++ b/mingling_core/src/renderer/structural.rs @@ -1,5 +1,5 @@ use crate::{ - RenderResult, StructuralRendererSetting, + ProgramCollect, RenderResult, StructuralRendererSetting, renderer::structural::error::StructuralRendererSerializeError, }; use serde::Serialize; @@ -24,11 +24,15 @@ impl StructuralRenderer { /// /// Returns `Err(StructuralRendererSerializeError)` if serialization fails. #[allow(unused_variables)] - pub fn render<T: StructuralData + Send>( + pub fn render<T, C>( data: &T, setting: &StructuralRendererSetting, r: &mut RenderResult, - ) -> Result<(), StructuralRendererSerializeError> { + ) -> Result<(), StructuralRendererSerializeError> + where + T: StructuralData<C> + Send, + C: ProgramCollect<Enum = C>, + { match setting { StructuralRendererSetting::Disable => Ok(()), #[cfg(feature = "json_serde_fmt")] @@ -148,7 +152,7 @@ impl StructuralRenderer { #[cfg(test)] mod tests { use super::*; - use crate::RenderResult; + use crate::{MockProgramCollect, RenderResult}; use serde::Serialize; #[derive(Debug, Clone, PartialEq, Serialize)] @@ -157,8 +161,8 @@ mod tests { value: i32, } - impl crate::__private::StructuralDataSealed for TestData {} - impl StructuralData for TestData {} + impl crate::__private::StructuralDataSealed<MockProgramCollect> for TestData {} + impl StructuralData<MockProgramCollect> for TestData {} fn test_data() -> TestData { TestData { 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_core/src/renderer/structural/structural_data.rs b/mingling_core/src/renderer/structural/structural_data.rs index 1cafac3..583808c 100644 --- a/mingling_core/src/renderer/structural/structural_data.rs +++ b/mingling_core/src/renderer/structural/structural_data.rs @@ -1,5 +1,7 @@ use serde::Serialize; +use crate::ProgramCollect; + /// Marker trait for types that support structured output (JSON / YAML / TOML / RON). /// /// This trait is a **supertrait** of `serde::Serialize` and is sealed via @@ -12,4 +14,8 @@ use serde::Serialize; /// These entry points also register the type in the global `STRUCTURED_TYPES` /// registry, which is required for the `structural_render` match arm to be generated. #[doc(hidden)] -pub trait StructuralData: Serialize + crate::__private::StructuralDataSealed {} +pub trait StructuralData<C>: Serialize + crate::__private::StructuralDataSealed<C> +where + C: ProgramCollect<Enum = C>, +{ +} diff --git a/mingling_core/tests/test-all/tests/integration.rs b/mingling_core/tests/test-all/tests/integration.rs index 3581702..d36b9df 100644 --- a/mingling_core/tests/test-all/tests/integration.rs +++ b/mingling_core/tests/test-all/tests/integration.rs @@ -1,13 +1,12 @@ use mingling::Flag; -use mingling::StructuralRenderer; -use mingling::StructuralRendererSetting; -use mingling::MockProgramCollect; use mingling::NextProcess; -use mingling::StructuralData; use mingling::Node; use mingling::Program; use mingling::RenderResult; use mingling::StringVec; +use mingling::StructuralData; +use mingling::StructuralRenderer; +use mingling::StructuralRendererSetting; use mingling::comp::{ShellContext, ShellFlag, Suggest}; use mingling::core_res::ResREPL; use mingling::hook::ProgramHook; @@ -86,7 +85,7 @@ fn test_render_result_default() { fn test_render_result_print() { let mut r = RenderResult::default(); r.print("hello"); - assert_eq!(&*r, "hello"); + assert_eq!(r.to_string().as_str(), "hello"); } // StructuralRenderer @@ -125,13 +124,13 @@ fn test_structural_renderer_json() { #[test] fn test_is_completing() { - let program: Program<MockProgramCollect> = Program::new_with_args(["app", "__comp"]); + let program: Program<crate::ThisProgram> = Program::new_with_args(["app", "__comp"]); assert!(program.is_completing()); } #[test] fn test_is_not_completing() { - let program: Program<MockProgramCollect> = Program::new_with_args(["app", "greet"]); + let program: Program<crate::ThisProgram> = Program::new_with_args(["app", "greet"]); assert!(!program.is_completing()); } @@ -141,7 +140,7 @@ fn test_is_not_completing() { fn test_hook_setup() { static CALLED: AtomicBool = AtomicBool::new(false); - let hook = ProgramHook::<MockProgramCollect>::empty().on_begin::<_, ()>(|_| { + let hook = ProgramHook::<crate::ThisProgram>::empty().on_begin::<_, ()>(|_| { CALLED.store(true, Ordering::SeqCst); }); @@ -166,3 +165,5 @@ fn test_string_vec_from_array() { let v: Vec<String> = sv.into(); assert_eq!(v, vec!["a", "b", "c"]); } + +mingling::macros::gen_program!(); diff --git a/mingling_core/tests/test-basic/tests/integration.rs b/mingling_core/tests/test-basic/tests/integration.rs index 7cd7b8c..e51992c 100644 --- a/mingling_core/tests/test-basic/tests/integration.rs +++ b/mingling_core/tests/test-basic/tests/integration.rs @@ -60,7 +60,7 @@ fn test_render_result_default() { fn test_render_result_print() { let mut r = RenderResult::default(); r.print("hello"); - assert_eq!(&*r, "hello"); + assert_eq!(r.to_string().as_str(), "hello"); } #[test] diff --git a/mingling_core/tests/test-dispatch-tree/tests/integration.rs b/mingling_core/tests/test-dispatch-tree/tests/integration.rs index 7cd7b8c..e51992c 100644 --- a/mingling_core/tests/test-dispatch-tree/tests/integration.rs +++ b/mingling_core/tests/test-dispatch-tree/tests/integration.rs @@ -60,7 +60,7 @@ fn test_render_result_default() { fn test_render_result_print() { let mut r = RenderResult::default(); r.print("hello"); - assert_eq!(&*r, "hello"); + assert_eq!(r.to_string().as_str(), "hello"); } #[test] diff --git a/mingling_core/tests/test-repl/tests/integration.rs b/mingling_core/tests/test-repl/tests/integration.rs index 1792525..4de0e8f 100644 --- a/mingling_core/tests/test-repl/tests/integration.rs +++ b/mingling_core/tests/test-repl/tests/integration.rs @@ -59,5 +59,5 @@ fn test_render_result_default() { fn test_render_result_print() { let mut r = RenderResult::default(); r.print("hello"); - assert_eq!(&*r, "hello"); + assert_eq!(r.to_string().as_str(), "hello"); } diff --git a/mingling_core/tests/test-structural-renderer/tests/integration.rs b/mingling_core/tests/test-structural-renderer/tests/integration.rs index 3c3c6db..e4057f8 100644 --- a/mingling_core/tests/test-structural-renderer/tests/integration.rs +++ b/mingling_core/tests/test-structural-renderer/tests/integration.rs @@ -1,4 +1,4 @@ -use mingling::{StructuralRenderer, StructuralRendererSetting, RenderResult, StructuralData}; +use mingling::{RenderResult, StructuralData, StructuralRenderer, StructuralRendererSetting}; use serde::Serialize; #[derive(Debug, Clone, PartialEq, Serialize, StructuralData)] @@ -17,7 +17,8 @@ fn test_data() -> TestData { #[test] fn test_render_disable() { let mut r = RenderResult::default(); - let result = StructuralRenderer::render(&test_data(), &StructuralRendererSetting::Disable, &mut r); + let result = + StructuralRenderer::render(&test_data(), &StructuralRendererSetting::Disable, &mut r); assert!(result.is_ok()); assert!(r.is_empty()); } @@ -73,3 +74,6 @@ fn test_render_ron() { assert!(output.contains("value:")); assert!(output.contains("42")); } + +mingling::macros::gen_program!(); + diff --git a/mingling_macros/src/attr/chain.rs b/mingling_macros/src/attr/chain.rs index 120e65d..dc28a39 100644 --- a/mingling_macros/src/attr/chain.rs +++ b/mingling_macros/src/attr/chain.rs @@ -34,27 +34,72 @@ 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, ) -> proc_macro2::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(); + // 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. + // For non-unit returns, wrap in `to_chain()` so mutable-resource closures + // return `ChainProcess<C>` (as required by `__modify_res_and_return_route`). + let fn_call_expr: syn::Expr = if is_unit_return { + syn::parse_quote! { #fn_call } + } else { + syn::parse_quote! { ::mingling::Routable::<#program_type>::to_chain(#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 +107,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,23 +127,16 @@ 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< - ::mingling::ChainProcess<#program_type> - >>::into(__chain_result) + ::mingling::Routable::<#program_type>::to_chain(__chain_result) } }; #[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 +145,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 } } @@ -192,13 +230,12 @@ pub(crate) 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; @@ -215,33 +252,23 @@ pub(crate) fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream { // Always use the default crate-defined program path let program_type = crate::default_program_path(); - // Extract the user's return type for the explicit Into turbofish - let origin_return_type = match &input_fn.sig.output { - ReturnType::Type(_, ty) => quote! { #ty }, - ReturnType::Default => quote! { () }, - }; - // 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"))] false, is_unit_return, - &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 }; diff --git a/mingling_macros/src/attr/completion.rs b/mingling_macros/src/attr/completion.rs index e917d7d..3ced091 100644 --- a/mingling_macros/src/attr/completion.rs +++ b/mingling_macros/src/attr/completion.rs @@ -47,11 +47,8 @@ pub(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre // 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(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre .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(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre // 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(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre // 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(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre 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(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStre 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/attr/dispatcher_clap.rs b/mingling_macros/src/attr/dispatcher_clap.rs index 6083a52..40f7d47 100644 --- a/mingling_macros/src/attr/dispatcher_clap.rs +++ b/mingling_macros/src/attr/dispatcher_clap.rs @@ -108,23 +108,23 @@ pub(crate) fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> Toke 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) } }; diff --git a/mingling_macros/src/attr/help.rs b/mingling_macros/src/attr/help.rs index 6defae4..aa7bc88 100644 --- a/mingling_macros/src/attr/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}; @@ -25,8 +25,8 @@ pub(crate) 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(crate) 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(crate) 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(crate) 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(crate) 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)* diff --git a/mingling_macros/src/attr/renderer.rs b/mingling_macros/src/attr/renderer.rs index c7cbb0b..828dc00 100644 --- a/mingling_macros/src/attr/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}; @@ -31,8 +31,8 @@ pub(crate) 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(crate) 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(crate) 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)* diff --git a/mingling_macros/src/derive/grouped.rs b/mingling_macros/src/derive/grouped.rs index 307aab6..a00eea1 100644 --- a/mingling_macros/src/derive/grouped.rs +++ b/mingling_macros/src/derive/grouped.rs @@ -16,7 +16,11 @@ pub(crate) fn derive_grouped(input: TokenStream) -> TokenStream { let expanded = quote! { ::mingling::macros::register_type!(#struct_name); - impl ::mingling::Grouped<#group_ident> for #struct_name { + /// SAFETY: This is an internal implementation of the `Grouped` derive macro, + /// guaranteeing that the enum value registered by the `register_type!` macro + /// is exactly the same as the actual return value, + /// which can be confirmed via the `Ident` in the `quote!` block. + unsafe impl ::mingling::Grouped<#group_ident> for #struct_name { fn member_id() -> #group_ident { #group_ident::#struct_name } @@ -46,7 +50,11 @@ pub fn derive_grouped_serialize(input: TokenStream) -> TokenStream { ::mingling::macros::register_type!(#struct_name); - impl ::mingling::Grouped<#group_ident> for #struct_name { + /// SAFETY: This is an internal implementation of the `Grouped` derive macro, + /// guaranteeing that the enum value registered by the `register_type!` macro + /// is exactly the same as the actual return value, + /// which can be confirmed via the `Ident` in the `quote!` block. + unsafe impl ::mingling::Grouped<#group_ident> for #struct_name { fn member_id() -> #group_ident { #group_ident::#struct_name } diff --git a/mingling_macros/src/extensions.rs b/mingling_macros/src/extensions.rs index aff20e2..c2fdb30 100644 --- a/mingling_macros/src/extensions.rs +++ b/mingling_macros/src/extensions.rs @@ -13,6 +13,13 @@ use syn::{Ident, Token}; #[cfg(feature = "extra_macros")] pub(crate) mod routeify; +/// Extension: `#[renderify]` — transforms `expr?` into `render_route!(expr)`. +#[cfg(feature = "extra_macros")] +pub(crate) mod renderify; + +/// 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>, 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/renderify.rs b/mingling_macros/src/extensions/renderify.rs new file mode 100644 index 0000000..9be8acc --- /dev/null +++ b/mingling_macros/src/extensions/renderify.rs @@ -0,0 +1,48 @@ +//! The `#[renderify]` extension — transforms `expr?` into `render_route!(expr)`. +//! +//! Designed as an extension for the Mingling attribute macro system, intended +//! to be used with `#[renderer(renderify)]`, `#[help(renderify)]`, +//! or standalone as `#[renderify]`. +//! +//! # How it works +//! +//! The macro parses the function AST and replaces every `Expr::Try` node with an +//! equivalent `render_route!(expr)` invocation, which routes errors to the +//! rendering pipeline via `crate::ThisProgram::render(AnyOutput::new(e))`. + +use proc_macro::TokenStream; +use quote::ToTokens; +use syn::spanned::Spanned; +use syn::visit_mut::VisitMut; +use syn::{Expr, ItemFn, parse_macro_input}; + +struct RenderifyTransform; + +impl VisitMut for RenderifyTransform { + 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 `render_route` ident to the `?` token's span, + // so that rust-analyzer resolves the `?` position to the `render_route!` macro + // instead of the standard Try trait, showing the render_route macro's docs on hover. + let q_span = try_expr.question_token.span(); + let route_ident = proc_macro2::Ident::new("render_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 renderify_impl(_attr: TokenStream, item: TokenStream) -> TokenStream { + let mut input_fn = parse_macro_input!(item as ItemFn); + RenderifyTransform.visit_item_fn_mut(&mut input_fn); + input_fn.to_token_stream().into() +} diff --git a/mingling_macros/src/extensions/routeify.rs b/mingling_macros/src/extensions/routeify.rs index f011fb9..7cded54 100644 --- a/mingling_macros/src/extensions/routeify.rs +++ b/mingling_macros/src/extensions/routeify.rs @@ -10,6 +10,7 @@ use proc_macro::TokenStream; use quote::ToTokens; +use syn::spanned::Spanned; use syn::visit_mut::VisitMut; use syn::{Expr, ItemFn, parse_macro_input}; @@ -23,8 +24,14 @@ impl VisitMut for RouteifyTransform { 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!(#inner_tokens) + ::mingling::macros::#route_ident!(#inner_tokens) }) { *expr = macro_expr; } diff --git a/mingling_macros/src/func.rs b/mingling_macros/src/func.rs index 720b20a..33bb094 100644 --- a/mingling_macros/src/func.rs +++ b/mingling_macros/src/func.rs @@ -8,5 +8,6 @@ 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/func/gen_program.rs b/mingling_macros/src/func/gen_program.rs index 7128a9a..db82504 100644 --- a/mingling_macros/src/func/gen_program.rs +++ b/mingling_macros/src/func/gen_program.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use proc_macro::TokenStream; use quote::quote; use syn::parse_macro_input; @@ -38,35 +40,33 @@ fn parse_entry_pair(entry: &proc_macro2::TokenStream) -> (proc_macro2::Ident, pr } /// 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> { +/// 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 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(), + 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. @@ -138,7 +138,7 @@ pub(crate) fn program_comp_gen_impl(_input: TokenStream) -> TokenStream { match read_ctx { Ok(ctx) => { let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx); - crate::CompletionSuggest::new((ctx, suggest)).to_render() + ::mingling::Routable::<crate::ThisProgram>::to_render(crate::CompletionSuggest::new((ctx, suggest))) } Err(_) => std::process::exit(1), } @@ -156,7 +156,7 @@ pub(crate) fn program_comp_gen_impl(_input: TokenStream) -> TokenStream { match read_ctx { Ok(ctx) => { let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx); - crate::CompletionSuggest::new((ctx, suggest)).to_render() + ::mingling::Routable::<crate::ThisProgram>::to_render(crate::CompletionSuggest::new((ctx, suggest))) } Err(_) => std::process::exit(1), } @@ -289,10 +289,34 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { .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() + 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 { - std::collections::HashMap::new() + 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") { @@ -318,7 +342,7 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { ) -> Result<::mingling::RenderResult, ::mingling::error::StructuralRendererSerializeError> { #[allow(unused_imports)] #(#pathf_uses)* - match any.member_id { + match any.member_id() { #(#structural_renderer_tokens)* _ => { // Non-structural types: render ResultEmpty (which implements @@ -384,7 +408,7 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { fn do_comp(any: &::mingling::AnyOutput<Self::Enum>, ctx: &::mingling::ShellContext) -> ::mingling::Suggest { #[allow(unused_imports)] #(#pathf_uses)* - match any.member_id { + match any.member_id() { #(#completion_tokens)* _ => ::mingling::Suggest::FileCompletion, } @@ -418,7 +442,7 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { }).collect(); quote! { fn render(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult { - match any.member_id { + match any.member_id() { #(#render_arms)* _ => ::mingling::RenderResult::default(), } @@ -470,9 +494,9 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { 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 { + match any.member_id() { #(#chain_arms_async)* - _ => ::core::panic!("No chain found for type id: {:?}", any.type_id), + _ => ::core::panic!("No chain found for type id: {:?}", any.type_id()), } } } @@ -481,9 +505,9 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { fn do_chain( any: ::mingling::AnyOutput<Self::Enum>, ) -> ::mingling::ChainProcess<Self::Enum> { - match any.member_id { + match any.member_id() { #(#chain_arms_sync)* - _ => ::core::panic!("No chain found for type id: {:?}", any.type_id), + _ => ::core::panic!("No chain found for type id: {:?}", any.type_id()), } } } @@ -509,7 +533,9 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { }; let expanded = quote! { - #[derive(Debug, PartialEq, Eq, Clone)] + #pathf_hint + + #[derive(Debug, PartialEq, Eq, Clone, Copy)] #[repr(#repr_type)] #[allow(nonstandard_style)] pub enum #name { @@ -543,19 +569,19 @@ pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream { fn render_help(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult { #[allow(unused_imports)] #(#pathf_uses)* - match any.member_id { + match any.member_id() { #(#help_tokens)* _ => ::mingling::RenderResult::default(), } } fn has_renderer(any: &::mingling::AnyOutput<Self::Enum>) -> bool { - match any.member_id { + match any.member_id() { #(#renderer_exist_tokens)* _ => false } } fn has_chain(any: &::mingling::AnyOutput<Self::Enum>) -> bool { - match any.member_id { + match any.member_id() { #(#chain_exist_tokens)* _ => false } diff --git a/mingling_macros/src/func/group.rs b/mingling_macros/src/func/group.rs index b865913..edb1fe1 100644 --- a/mingling_macros/src/func/group.rs +++ b/mingling_macros/src/func/group.rs @@ -133,7 +133,11 @@ pub(crate) fn group_macro(input: TokenStream) -> TokenStream { #type_use #alias_use - impl ::mingling::Grouped<__MinglingProgram> for #type_name { + /// SAFETY: This is an internal implementation of the `group!` macro, + /// guaranteeing that the enum value registered by the `register_type!` macro + /// is exactly the same as the actual return value, + /// which can be confirmed via the `Ident` in the `quote!` block. + unsafe impl ::mingling::Grouped<__MinglingProgram> for #type_name { fn member_id() -> __MinglingProgram { __MinglingProgram::#type_name } diff --git a/mingling_macros/src/func/pack.rs b/mingling_macros/src/func/pack.rs index a1a7e6b..7206b8e 100644 --- a/mingling_macros/src/func/pack.rs +++ b/mingling_macros/src/func/pack.rs @@ -138,7 +138,11 @@ pub(crate) fn pack(input: TokenStream) -> TokenStream { } } - impl ::mingling::Grouped<#group_name> for #type_name { + /// SAFETY: This is an internal implementation of the `pack!` macro, + /// guaranteeing that the enum value registered by the `register_type!` macro + /// is exactly the same as the actual return value, + /// which can be confirmed via the `Ident` in the `quote!` block. + unsafe impl ::mingling::Grouped<#group_name> for #type_name { fn member_id() -> #group_name { #group_name::#type_name } diff --git a/mingling_macros/src/func/pack_err.rs b/mingling_macros/src/func/pack_err.rs index 36e550a..2a318bc 100644 --- a/mingling_macros/src/func/pack_err.rs +++ b/mingling_macros/src/func/pack_err.rs @@ -139,8 +139,8 @@ pub(crate) fn pack_err_structural(input: TokenStream) -> TokenStream { .insert(type_name_str); let structural_data = quote! { - impl ::mingling::__private::StructuralDataSealed for #type_name {} - impl ::mingling::__private::StructuralData for #type_name {} + impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram> for #type_name {} + impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {} }; // Generate the struct + impls (same as pack_err! but with Serialize derive + sealed) diff --git a/mingling_macros/src/func/r_print.rs b/mingling_macros/src/func/r_print.rs new file mode 100644 index 0000000..86ea52f --- /dev/null +++ b/mingling_macros/src/func/r_print.rs @@ -0,0 +1,127 @@ +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") +} + +pub(crate) fn r_eprintln(input: TokenStream) -> TokenStream { + expand_print(input, "eprintln") +} + +pub(crate) fn r_eprint(input: TokenStream) -> TokenStream { + expand_print(input, "eprint") +} + +/// Parsed input for `r_append!`. +/// +/// Two forms: +/// - `(dst, src)` — explicit buffer and source +/// - `(src)` — implicit `__render_result_buffer` +struct AppendInput { + dst: Option<Ident>, + src: proc_macro2::TokenStream, +} + +impl Parse for AppendInput { + fn parse(input: ParseStream) -> syn::Result<Self> { + if input.peek(Ident) && input.peek2(Token![,]) { + let dst: Ident = input.parse()?; + let _comma: Token![,] = input.parse()?; + let src: TokenStream2 = input.parse()?; + Ok(AppendInput { + dst: Some(dst), + src, + }) + } else { + let src: TokenStream2 = input.parse()?; + Ok(AppendInput { dst: None, src }) + } + } +} + +/// `r_append!` macro: appends the contents of another `RenderResult` to this one. +/// +/// Two forms: +/// - `r_append!(dst, src)` — appends `src` into `dst` +/// - `r_append!(src)` — appends `src` into `__render_result_buffer` +pub(crate) fn r_append(input: TokenStream) -> TokenStream { + let parsed: AppendInput = match syn::parse(input) { + Ok(p) => p, + Err(e) => return e.to_compile_error().into(), + }; + + let dst_ident = parsed.dst.clone(); + let src_tokens = parsed.src; + + let expanded = match dst_ident { + Some(dst) => { + quote! { + #dst.append_other(#src_tokens); + } + } + None => { + quote! { + __render_result_buffer.append_other(#src_tokens); + } + } + }; + + expanded.into() +} diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index a85648f..ca3261e 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -141,6 +141,8 @@ //! } //! ``` +#![deny(missing_docs)] + #[cfg(feature = "extra_macros")] use quote::quote; @@ -270,6 +272,36 @@ fn entry_has_variant(entry: &str, variant_name: &str) -> bool { entry.contains(&format!(":: {variant_name} =>")) } +/// 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 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 +/// // Simple form — creates a wrapper named after the type's last segment: +/// group!(ParseIntError); +/// +/// // Aliased form — creates a wrapper with a custom name: +/// group!(ErrorIo = std::io::Error); +/// ``` +/// +/// # Example +/// +/// 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 +/// ``` +/// +/// # Requirements +/// +/// - 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 { @@ -511,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 @@ -536,6 +590,56 @@ pub fn route(input: TokenStream) -> TokenStream { TokenStream::from(expanded) } +/// Routes errors to the rendering pipeline instead of the chain pipeline. +/// +/// This macro is similar to [`route!`] but instead of routing errors through +/// `Routable::to_chain()` (which returns `ChainProcess`), it routes them +/// directly to the renderer via `crate::ThisProgram::render(AnyOutput::new(e))` +/// (which returns `RenderResult`). +/// +/// This is useful in `#[renderer]` and `#[help]` functions where the return +/// type is `RenderResult` rather than `ChainProcess`. +/// +/// # Syntax +/// +/// ```rust,ignore +/// render_route!(expr) +/// ``` +/// +/// Where `expr` is an expression of type `Result<T, E>`. +/// +/// # Interaction with `#[routeify]` +/// +/// When `#[routeify]` is used on a `#[renderer]` or `#[help]` function (e.g. +/// `#[renderer(routeify)]` or `#[help(routeify)]`), every `expr?` is automatically +/// replaced with `render_route!(expr)` instead of `route!(expr)`. +/// +/// # Example +/// +/// ```rust,ignore +/// use mingling::macros::{renderer, render_route}; +/// use std::io::Write; +/// +/// #[renderer] +/// fn render_something(prev: SomeType) -> RenderResult { +/// let data = render_route!(fetch_data().map_err(|e| ErrorEntry::new(e.to_string())))?; +/// // ... render data +/// Ok(RenderResult::new()) +/// } +/// ``` +#[cfg(feature = "extra_macros")] +#[proc_macro] +pub fn render_route(input: TokenStream) -> TokenStream { + let expr = parse_macro_input!(input as syn::Expr); + let expanded = quote! { + match #expr { + Ok(r) => r, + Err(e) => return <crate::ThisProgram as ::mingling::ProgramCollect>::render(::mingling::AnyOutput::new(e)), + } + }; + TokenStream::from(expanded) +} + /// Creates an empty result value wrapped in `ChainProcess` for early return /// from a chain function. /// @@ -588,7 +692,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) } @@ -1239,6 +1343,25 @@ pub fn help(attr: TokenStream, item: TokenStream) -> TokenStream { help::help_attr(item) } +/// Marker attribute for the Mingling lint system. +/// +/// The content of this attribute is ignored by rustc and reserved for +/// the mlint tool to interpret. All it does is pass the item through +/// unchanged. +/// +/// # Examples +/// +/// ```rust,ignore +/// #[mlint(allow(MLINT_SOME_LINT))] +/// #[mlint(warn(MLINT_SOME_LINT))] +/// #[mlint(deny(MLINT_SOME_LINT))] +/// fn some_item() {} +/// ``` +#[proc_macro_attribute] +pub fn mlint(_attr: TokenStream, item: TokenStream) -> TokenStream { + item +} + /// Extension attribute macro that transforms `expr?` into `route!(expr)`. /// /// Designed for use with `#[chain(routeify, ...)]` to enable concise error @@ -1260,6 +1383,226 @@ pub fn routeify(attr: TokenStream, item: TokenStream) -> TokenStream { extensions::routeify::routeify_impl(attr, item) } +/// Extension attribute macro that transforms `expr?` into `render_route!(expr)`. +/// +/// Designed for use with `#[renderer(renderify, ...)]` or `#[help(renderify, ...)]` +/// to enable concise error routing in renderer and help functions using the `?` +/// operator syntax. +/// +/// Unlike `#[routeify]` which routes errors to the chain pipeline via `route!`, +/// `#[renderify]` routes errors to the rendering pipeline via `render_route!`, +/// which matches the `RenderResult` return type of renderer and help functions. +/// +/// # Example +/// +/// ```rust,ignore +/// #[renderer(renderify)] +/// fn render_greeting(prev: Greeting) -> RenderResult { +/// let data = load_data()?; // expands to render_route!(load_data()) +/// r_println!("{data}"); +/// Ok(RenderResult::new()) +/// } +/// ``` +#[cfg(feature = "extra_macros")] +#[proc_macro_attribute] +pub fn renderify(attr: TokenStream, item: TokenStream) -> TokenStream { + extensions::renderify::renderify_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) +} + +/// Prints text to a `RenderResult` buffer (standard error style), with a trailing newline. +/// +/// This macro works identically to `r_println!` but conceptually targets +/// "error output" — it writes into a `RenderResult` buffer with a trailing newline. +/// +/// # Implicit buffer (inside `#[buffer]` functions) +/// +/// ```rust,ignore +/// use mingling::macros::{buffer, r_eprintln}; +/// +/// #[buffer] +/// fn render() { +/// r_eprintln!("Error: {}", err_msg); +/// } +/// ``` +/// +/// # Explicit buffer +/// +/// Pass a `RenderResult` variable as the first argument: +/// +/// ```rust,ignore +/// use mingling::macros::r_eprintln; +/// use mingling::RenderResult; +/// +/// let mut r = RenderResult::new(); +/// r_eprintln!(r, "error: {}", 42); +/// assert_eq!(&*r, "error: 42\n"); +/// ``` +#[proc_macro] +pub fn r_eprintln(input: TokenStream) -> TokenStream { + func::r_print::r_eprintln(input) +} + +/// Prints text to a `RenderResult` buffer (standard error style), without a trailing newline. +/// +/// This macro works identically to `r_print!` but conceptually targets +/// "error output" — it writes into a `RenderResult` buffer without a trailing newline. +/// +/// # Implicit buffer (inside `#[buffer]` functions) +/// +/// ```rust,ignore +/// use mingling::macros::{buffer, r_eprint}; +/// +/// #[buffer] +/// fn render() { +/// r_eprint!("Error: "); +/// r_eprintln!("something went wrong"); +/// } +/// ``` +/// +/// # Explicit buffer +/// +/// ```rust,ignore +/// use mingling::macros::r_eprint; +/// use mingling::RenderResult; +/// +/// let mut r = RenderResult::new(); +/// r_eprint!(r, "error: "); +/// r_eprintln!(r, "42"); +/// assert_eq!(&*r, "error: 42\n"); +/// ``` +#[proc_macro] +pub fn r_eprint(input: TokenStream) -> TokenStream { + func::r_print::r_eprint(input) +} + +/// Appends the contents of one `RenderResult` to another. +/// +/// # Implicit buffer (inside `#[buffer]` functions) +/// +/// ```rust,ignore +/// use mingling::macros::{buffer, r_append}; +/// +/// #[buffer] +/// fn render() { +/// let other = make_other_result(); +/// r_append!(other); +/// } +/// ``` +/// +/// # Explicit buffer +/// +/// ```rust,ignore +/// use mingling::macros::r_append; +/// use mingling::RenderResult; +/// +/// let mut dst = RenderResult::new(); +/// let src = RenderResult::from("hello"); +/// r_append!(dst, src); +/// assert!(!dst.is_empty()); +/// ``` +#[proc_macro] +pub fn r_append(input: TokenStream) -> TokenStream { + func::r_print::r_append(input) +} + /// Derive macro for automatically implementing the `Grouped` trait on a struct. /// /// The `#[derive(Grouped)]` macro: @@ -1450,6 +1793,28 @@ pub fn gen_program(input: TokenStream) -> TokenStream { func::gen_program::gen_program_impl(input) } +/// Internal macro used by `gen_program!` to generate the completion infrastructure for +/// shell completion support. +/// +/// **This macro is only available with the `comp` feature.** +/// +/// 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")] #[proc_macro] pub fn program_comp_gen(input: TokenStream) -> TokenStream { @@ -1481,16 +1846,47 @@ pub fn register_type(input: TokenStream) -> TokenStream { func::gen_program::register_type_impl(input) } +/// Registers a chain mapping function into the global chain registry. +/// +/// 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`. +/// +/// 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. +/// +/// # Panics +/// +/// Panics if the global `CHAINS` mutex is poisoned. #[proc_macro] pub fn register_chain(input: TokenStream) -> TokenStream { func::gen_program::register_chain_impl(input) } +/// Registers a renderer mapping function into the global renderer registry. +/// +/// 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`. +/// +/// The entry string format is a match arm: the type variant maps to a call +/// of the registered renderer function that produces a `RenderResult`. +/// +/// # Panics +/// +/// Panics if the global `RENDERERS` mutex is poisoned. #[proc_macro] pub fn register_renderer(input: TokenStream) -> TokenStream { func::gen_program::register_renderer_impl(input) } +/// 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. #[proc_macro] pub fn program_fallback_gen(input: TokenStream) -> TokenStream { func::gen_program::program_fallback_gen_impl(input) diff --git a/mingling_macros/src/systems/structural_data.rs b/mingling_macros/src/systems/structural_data.rs index 74bcf09..46d7cf8 100644 --- a/mingling_macros/src/systems/structural_data.rs +++ b/mingling_macros/src/systems/structural_data.rs @@ -29,8 +29,8 @@ pub(crate) fn derive_structural_data(input: TokenStream) -> TokenStream { // Users cannot implement StructuralDataSealed manually (it's #[doc(hidden)]), // so the only way to get StructuralData is through this derive macro. let expanded = quote! { - impl ::mingling::__private::StructuralDataSealed for #type_name {} - impl ::mingling::__private::StructuralData for #type_name {} + impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram> for #type_name {} + impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {} }; expanded.into() @@ -149,8 +149,8 @@ pub(crate) fn pack_structural(input: TokenStream) -> TokenStream { // StructuralData impl + sealed + registration let structural_impl = quote! { - impl ::mingling::__private::StructuralDataSealed for #type_name {} - impl ::mingling::__private::StructuralData for #type_name {} + impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram> for #type_name {} + impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {} }; let expanded = quote! { @@ -176,7 +176,11 @@ pub(crate) fn pack_structural(input: TokenStream) -> TokenStream { } } - impl ::mingling::Grouped<#program_path> for #type_name { + /// SAFETY: This is an internal implementation of the `pack_structural!` macro, + /// guaranteeing that the enum value registered by the `register_type!` macro + /// is exactly the same as the actual return value, + /// which can be confirmed via the `Ident` in the `quote!` block. + unsafe impl ::mingling::Grouped<#program_path> for #type_name { fn member_id() -> #program_path { #program_path::#type_name } @@ -297,14 +301,18 @@ pub(crate) fn group_structural(input: TokenStream) -> TokenStream { #type_use #alias_use - impl ::mingling::Grouped<__MinglingProgram> for #type_name { + /// SAFETY: This is an internal implementation of the `pack_structural!` macro, + /// guaranteeing that the enum value registered by the `register_type!` macro + /// is exactly the same as the actual return value, + /// which can be confirmed via the `Ident` in the `quote!` block. + unsafe impl ::mingling::Grouped<__MinglingProgram> for #type_name { fn member_id() -> __MinglingProgram { __MinglingProgram::#type_name } } - impl ::mingling::__private::StructuralDataSealed for #type_name {} - impl ::mingling::__private::StructuralData for #type_name {} + impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram> for #type_name {} + impl ::mingling::__private::StructuralData<crate::ThisProgram> for #type_name {} ::mingling::macros::register_type!(#type_name); } 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..5b25c15 100644 --- a/mingling_pathf/src/pattern_analyzer.rs +++ b/mingling_pathf/src/pattern_analyzer.rs @@ -48,6 +48,28 @@ pub struct AnalyzeItem { pub module: String, /// The name of the item itself, e.g. `"HashMap"`, `"AnalyzeResult"`, etc. pub item_name: String, + /// Whether the item is from an external crate (resolved via `use`), bypassing the file's own module path. + pub is_foreign: bool, +} + +impl AnalyzeItem { + /// Creates a local `AnalyzeItem` (not foreign, will be prefixed with the file's module path). + pub fn local(module: String, item_name: String) -> Self { + Self { + module, + item_name, + is_foreign: false, + } + } + + /// Creates a foreign `AnalyzeItem` (resolved via `use`, the `module` field is the full import path). + pub fn foreign(module: String, item_name: String) -> Self { + Self { + module, + item_name, + is_foreign: true, + } + } } /// Collection of analysis results @@ -100,10 +122,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/basic_struct.rs b/mingling_pathf/src/patterns/basic_struct.rs index 09e8e70..9218d65 100644 --- a/mingling_pathf/src/patterns/basic_struct.rs +++ b/mingling_pathf/src/patterns/basic_struct.rs @@ -28,20 +28,17 @@ impl AnalyzePattern for BasicStructPattern { match item { // Root-level struct Item::Struct(s) => { - items.push(AnalyzeItem { - module: String::new(), - item_name: s.ident.to_string(), - }); + items.push(AnalyzeItem::local(String::new(), s.ident.to_string())); } // Struct within inline modules Item::Mod(item_mod) => { if let Some((_, nested)) = &item_mod.content { for n in nested { if let syn::Item::Struct(s) = n { - items.push(AnalyzeItem { - module: item_mod.ident.to_string(), - item_name: s.ident.to_string(), - }); + items.push(AnalyzeItem::local( + item_mod.ident.to_string(), + s.ident.to_string(), + )); } } } diff --git a/mingling_pathf/src/patterns/chain.rs b/mingling_pathf/src/patterns/chain.rs index 6393440..2e95db2 100644 --- a/mingling_pathf/src/patterns/chain.rs +++ b/mingling_pathf/src/patterns/chain.rs @@ -18,7 +18,7 @@ pub struct ChainPattern; impl AnalyzePattern for ChainPattern { fn contains(&self, content: &str) -> bool { - content.contains("chain]") + content.contains("[chain") || content.contains("chain]") } fn analyze(&self, content: &str) -> Vec<AnalyzeItem> { @@ -44,10 +44,10 @@ fn collect_from_item(item: &Item, current_mod: &str, items: &mut Vec<AnalyzeItem match item { Item::Fn(f) if has_attr(&f.attrs, "chain") => { let fn_name = f.sig.ident.to_string(); - items.push(AnalyzeItem { - module: current_mod.to_string(), - item_name: internal_name(&fn_name), - }); + items.push(AnalyzeItem::local( + current_mod.to_string(), + internal_name(&fn_name), + )); } Item::Mod(item_mod) => { if let Some((_, nested)) = &item_mod.content { diff --git a/mingling_pathf/src/patterns/completion.rs b/mingling_pathf/src/patterns/completion.rs index 5427b93..cff62e0 100644 --- a/mingling_pathf/src/patterns/completion.rs +++ b/mingling_pathf/src/patterns/completion.rs @@ -13,7 +13,7 @@ pub struct CompletionPattern; impl AnalyzePattern for CompletionPattern { fn contains(&self, content: &str) -> bool { - content.contains("completion(") + content.contains("completion(") || content.contains("[completion") } fn analyze(&self, content: &str) -> Vec<AnalyzeItem> { @@ -37,10 +37,10 @@ fn collect_from_item(item: &Item, current_mod: &str, items: &mut Vec<AnalyzeItem match item { Item::Fn(f) if has_attr(&f.attrs, "completion") => { let fn_name = f.sig.ident.to_string(); - items.push(AnalyzeItem { - module: current_mod.to_string(), - item_name: internal_name(&fn_name), - }); + items.push(AnalyzeItem::local( + current_mod.to_string(), + internal_name(&fn_name), + )); } Item::Mod(item_mod) => { if let Some((_, nested)) = &item_mod.content { diff --git a/mingling_pathf/src/patterns/dispatcher.rs b/mingling_pathf/src/patterns/dispatcher.rs index 6796a2c..cedad9f 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 } } @@ -103,27 +111,18 @@ fn extract_all_types( // Entry type — always if let Some(ref entry) = entry_struct { - items.push(AnalyzeItem { - module: module.to_string(), - item_name: entry.clone(), - }); + items.push(AnalyzeItem::local(module.to_string(), entry.clone())); } // CMD type — always if let Some(ref cmd) = cmd_struct { - items.push(AnalyzeItem { - module: module.to_string(), - item_name: cmd.clone(), - }); + items.push(AnalyzeItem::local(module.to_string(), cmd.clone())); } // __internal_dispatcher_* — when configured if use_dispatch_tree { let internal_name = format!("__internal_dispatcher_{}", snake_case(&cmd_name)); - items.push(AnalyzeItem { - module: module.to_string(), - item_name: internal_name, - }); + items.push(AnalyzeItem::local(module.to_string(), internal_name)); } items diff --git a/mingling_pathf/src/patterns/dispatcher_clap.rs b/mingling_pathf/src/patterns/dispatcher_clap.rs index 1a86ad5..25a7093 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 } } @@ -55,10 +61,7 @@ impl AnalyzePattern for DispatcherClapPattern { Item::Struct(s) if has_attr(&s.attrs, "dispatcher_clap") => { // Entry type (struct name) — always let entry_name = s.ident.to_string(); - items.push(AnalyzeItem { - module: String::new(), - item_name: entry_name.clone(), - }); + items.push(AnalyzeItem::local(String::new(), entry_name.clone())); // Parse the attribute to extract CMD, error, and help info if let Some(attr) = s.attrs.iter().find(|a| { @@ -73,18 +76,12 @@ impl AnalyzePattern for DispatcherClapPattern { // CMD type — always if let Some(ref cmd) = parsed.cmd_type { - items.push(AnalyzeItem { - module: String::new(), - item_name: cmd.clone(), - }); + items.push(AnalyzeItem::local(String::new(), cmd.clone())); } // Error type — if error = TypeName if let Some(ref err) = parsed.error_type { - items.push(AnalyzeItem { - module: String::new(), - item_name: err.clone(), - }); + items.push(AnalyzeItem::local(String::new(), err.clone())); } // Help internal struct — if help = true @@ -94,10 +91,7 @@ impl AnalyzePattern for DispatcherClapPattern { let help_fn = format!("__{}_help", just_fmt::snake_case!(cmd)); let help_struct = format!("__internal_help_{}", just_fmt::snake_case!(&help_fn)); - items.push(AnalyzeItem { - module: String::new(), - item_name: help_struct, - }); + items.push(AnalyzeItem::local(String::new(), help_struct)); } // __internal_dispatcher_* — when configured @@ -108,10 +102,7 @@ impl AnalyzePattern for DispatcherClapPattern { "__internal_dispatcher_{}", just_fmt::snake_case!(cmd_name) ); - items.push(AnalyzeItem { - module: String::new(), - item_name: internal_name, - }); + items.push(AnalyzeItem::local(String::new(), internal_name)); } } } @@ -122,10 +113,10 @@ impl AnalyzePattern for DispatcherClapPattern { && has_attr(&s.attrs, "dispatcher_clap") { let entry_name = s.ident.to_string(); - items.push(AnalyzeItem { - module: item_mod.ident.to_string(), - item_name: entry_name.clone(), - }); + items.push(AnalyzeItem::local( + item_mod.ident.to_string(), + entry_name.clone(), + )); if let Some(attr) = s.attrs.iter().find(|a| { a.path() @@ -139,17 +130,17 @@ impl AnalyzePattern for DispatcherClapPattern { let parsed = parse_dispatcher_clap_args(&args_str); if let Some(ref cmd) = parsed.cmd_type { - items.push(AnalyzeItem { - module: item_mod.ident.to_string(), - item_name: cmd.clone(), - }); + items.push(AnalyzeItem::local( + item_mod.ident.to_string(), + cmd.clone(), + )); } if let Some(ref err) = parsed.error_type { - items.push(AnalyzeItem { - module: item_mod.ident.to_string(), - item_name: err.clone(), - }); + items.push(AnalyzeItem::local( + item_mod.ident.to_string(), + err.clone(), + )); } // Help internal struct — same naming rule as root level @@ -162,10 +153,10 @@ impl AnalyzePattern for DispatcherClapPattern { "__internal_help_{}", just_fmt::snake_case!(&help_fn) ); - items.push(AnalyzeItem { - module: item_mod.ident.to_string(), - item_name: help_struct, - }); + items.push(AnalyzeItem::local( + item_mod.ident.to_string(), + help_struct, + )); } // __internal_dispatcher_* — when configured @@ -176,10 +167,10 @@ impl AnalyzePattern for DispatcherClapPattern { "__internal_dispatcher_{}", just_fmt::snake_case!(cmd_name) ); - items.push(AnalyzeItem { - module: item_mod.ident.to_string(), - item_name: internal_name, - }); + items.push(AnalyzeItem::local( + item_mod.ident.to_string(), + internal_name, + )); } } } diff --git a/mingling_pathf/src/patterns/group.rs b/mingling_pathf/src/patterns/group.rs index 0e4b50d..0f9ecff 100644 --- a/mingling_pathf/src/patterns/group.rs +++ b/mingling_pathf/src/patterns/group.rs @@ -2,7 +2,10 @@ //! extracts the type name or alias defined within them. //! This is used to track type groups for code generation or analysis. +use std::collections::HashMap; + use syn::Item; +use syn::UseTree; use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; @@ -25,6 +28,9 @@ impl AnalyzePattern for GroupPattern { return Vec::new(); }; + // Collect `use` imports at the file level + let imports = collect_use_imports(&syntax.items); + let mut items = Vec::new(); for item in &syntax.items { @@ -37,15 +43,15 @@ impl AnalyzePattern for GroupPattern { if macro_name != "group" && macro_name != "group_structural" { continue; } - if let Some(name) = extract_group_name(&m.mac.tokens) { - items.push(AnalyzeItem { - module: String::new(), - item_name: name, - }); + if let Some(analyze_item) = extract_group_item(&m.mac.tokens, &imports, "") { + items.push(analyze_item); } } Item::Mod(item_mod) => { if let Some((_, nested)) = &item_mod.content { + // Collect `use` imports inside this module + let inner_imports = collect_use_imports(nested); + for n in nested { if let Item::Macro(m) = n { let Some(last) = m.mac.path.segments.last() else { @@ -55,11 +61,12 @@ impl AnalyzePattern for GroupPattern { if macro_name != "group" && macro_name != "group_structural" { continue; } - if let Some(name) = extract_group_name(&m.mac.tokens) { - items.push(AnalyzeItem { - module: item_mod.ident.to_string(), - item_name: name, - }); + if let Some(analyze_item) = extract_group_item( + &m.mac.tokens, + &inner_imports, + &item_mod.ident.to_string(), + ) { + items.push(analyze_item); } } } @@ -73,6 +80,95 @@ impl AnalyzePattern for GroupPattern { } } +/// Extract an `AnalyzeItem` from the macro invocation tokens. +/// +/// If the name matches a `use` import, resolves to the full foreign path. +/// For the `Alias = path::Type` form, returns a local item (the alias exists in-crate). +fn extract_group_item( + tokens: &proc_macro2::TokenStream, + imports: &HashMap<String, (String, String)>, + current_mod: &str, +) -> Option<AnalyzeItem> { + let name = extract_group_name(tokens)?; + let is_aliased = has_equals_sign(tokens); + + if is_aliased { + // `group!(Alias = path::Type)` — alias lives in-crate + Some(AnalyzeItem::local(current_mod.to_string(), name)) + } else if let Some((module, _)) = imports.get(&name) { + // `group!(TypeName)` where TypeName is imported via `use` — foreign + Some(AnalyzeItem::foreign(module.clone(), name)) + } else { + // `group!(LocalType)` — local type + Some(AnalyzeItem::local(current_mod.to_string(), name)) + } +} + +/// Collect `use` imports from a list of top-level items. +/// +/// Returns a map of `short_name → (module_path, short_name)`. +/// e.g. `use cargo_metadata::CompilerMessage;` → `"CompilerMessage" → ("cargo_metadata", "CompilerMessage")` +fn collect_use_imports(items: &[syn::Item]) -> HashMap<String, (String, String)> { + let mut map = HashMap::new(); + for item in items { + if let Item::Use(use_item) = item { + // Only handle non-`pub use` (regular imports) + collect_from_use_tree(&use_item.tree, "", &mut map); + } + } + map +} + +/// Recursively traverse a `UseTree` and collect named imports. +fn collect_from_use_tree( + tree: &UseTree, + prefix: &str, + map: &mut HashMap<String, (String, String)>, +) { + match tree { + UseTree::Name(name) => { + let module = prefix.to_string(); + let alias = name.ident.to_string(); + map.entry(alias).or_insert((module, name.ident.to_string())); + } + UseTree::Path(use_path) => { + let new_prefix = if prefix.is_empty() { + use_path.ident.to_string() + } else { + format!("{}::{}", prefix, use_path.ident) + }; + collect_from_use_tree(&use_path.tree, &new_prefix, map); + } + UseTree::Rename(rename) => { + let module = prefix.to_string(); + let alias = rename.ident.to_string(); + map.entry(alias) + .or_insert((module, rename.ident.to_string())); + } + UseTree::Glob(_) => { + // `use path::*;` — skip glob imports + } + UseTree::Group(group) => { + for item in &group.items { + collect_from_use_tree(item, prefix, map); + } + } + } +} + +/// Check whether the macro tokens contain `=`, indicating aliased form. +fn has_equals_sign(tokens: &proc_macro2::TokenStream) -> bool { + let stream = tokens.clone(); + for token in stream { + if let proc_macro2::TokenTree::Punct(p) = token + && p.as_char() == '=' + { + return true; + } + } + false +} + /// Extract the alias / type name from the arguments of `group!`. /// /// - `group!(ParseIntError)` → `ParseIntError` diff --git a/mingling_pathf/src/patterns/grouped_derive.rs b/mingling_pathf/src/patterns/grouped_derive.rs index 9522c1f..56a87f9 100644 --- a/mingling_pathf/src/patterns/grouped_derive.rs +++ b/mingling_pathf/src/patterns/grouped_derive.rs @@ -30,38 +30,29 @@ impl AnalyzePattern for GroupedDerivePattern { for item in &syntax.items { match item { Item::Struct(s) if has_grouped_derive(&s.attrs) => { - items.push(AnalyzeItem { - module: String::new(), - item_name: s.ident.to_string(), - }); + items.push(AnalyzeItem::local(String::new(), s.ident.to_string())); } Item::Enum(e) if has_grouped_derive(&e.attrs) => { - items.push(AnalyzeItem { - module: String::new(), - item_name: e.ident.to_string(), - }); + items.push(AnalyzeItem::local(String::new(), e.ident.to_string())); } Item::Union(u) if has_grouped_derive(&u.attrs) => { - items.push(AnalyzeItem { - module: String::new(), - item_name: u.ident.to_string(), - }); + items.push(AnalyzeItem::local(String::new(), u.ident.to_string())); } Item::Mod(item_mod) => { if let Some((_, nested)) = &item_mod.content { for n in nested { match n { Item::Struct(s) if has_grouped_derive(&s.attrs) => { - items.push(AnalyzeItem { - module: item_mod.ident.to_string(), - item_name: s.ident.to_string(), - }); + items.push(AnalyzeItem::local( + item_mod.ident.to_string(), + s.ident.to_string(), + )); } Item::Enum(e) if has_grouped_derive(&e.attrs) => { - items.push(AnalyzeItem { - module: item_mod.ident.to_string(), - item_name: e.ident.to_string(), - }); + items.push(AnalyzeItem::local( + item_mod.ident.to_string(), + e.ident.to_string(), + )); } _ => {} } diff --git a/mingling_pathf/src/patterns/help.rs b/mingling_pathf/src/patterns/help.rs index 628f4ac..85793c3 100644 --- a/mingling_pathf/src/patterns/help.rs +++ b/mingling_pathf/src/patterns/help.rs @@ -13,7 +13,7 @@ pub struct HelpPattern; impl AnalyzePattern for HelpPattern { fn contains(&self, content: &str) -> bool { - content.contains("help]") + content.contains("[help") || content.contains("help]") } fn analyze(&self, content: &str) -> Vec<AnalyzeItem> { @@ -37,10 +37,10 @@ fn collect_from_item(item: &Item, current_mod: &str, items: &mut Vec<AnalyzeItem match item { Item::Fn(f) if has_attr(&f.attrs, "help") => { let fn_name = f.sig.ident.to_string(); - items.push(AnalyzeItem { - module: current_mod.to_string(), - item_name: internal_name(&fn_name), - }); + items.push(AnalyzeItem::local( + current_mod.to_string(), + internal_name(&fn_name), + )); } Item::Mod(item_mod) => { if let Some((_, nested)) = &item_mod.content { diff --git a/mingling_pathf/src/patterns/pack.rs b/mingling_pathf/src/patterns/pack.rs index c80fb65..76597ba 100644 --- a/mingling_pathf/src/patterns/pack.rs +++ b/mingling_pathf/src/patterns/pack.rs @@ -18,7 +18,10 @@ pub struct PackPattern; impl AnalyzePattern for PackPattern { fn contains(&self, content: &str) -> bool { - content.contains("pack!") || content.contains("pack_err!") + content.contains("pack!") + || content.contains("pack_err!") + || content.contains("pack_structural!") + || content.contains("pack_err_structural!") } fn analyze(&self, content: &str) -> Vec<AnalyzeItem> { @@ -33,10 +36,7 @@ impl AnalyzePattern for PackPattern { // Top-level macro calls Item::Macro(m) => { if let Some(name) = try_extract_pack_name(m) { - items.push(AnalyzeItem { - module: String::new(), - item_name: name, - }); + items.push(AnalyzeItem::local(String::new(), name)); } } // Macro calls inside inline modules @@ -46,10 +46,7 @@ impl AnalyzePattern for PackPattern { if let Item::Macro(m) = n && let Some(name) = try_extract_pack_name(m) { - items.push(AnalyzeItem { - module: item_mod.ident.to_string(), - item_name: name, - }); + items.push(AnalyzeItem::local(item_mod.ident.to_string(), name)); } } } diff --git a/mingling_pathf/src/patterns/renderer.rs b/mingling_pathf/src/patterns/renderer.rs index c2e9ca9..153e603 100644 --- a/mingling_pathf/src/patterns/renderer.rs +++ b/mingling_pathf/src/patterns/renderer.rs @@ -13,7 +13,7 @@ pub struct RendererPattern; impl AnalyzePattern for RendererPattern { fn contains(&self, content: &str) -> bool { - content.contains("renderer]") + content.contains("[renderer") || content.contains("renderer]") } fn analyze(&self, content: &str) -> Vec<AnalyzeItem> { @@ -37,10 +37,10 @@ fn collect_from_item(item: &Item, current_mod: &str, items: &mut Vec<AnalyzeItem match item { Item::Fn(f) if has_attr(&f.attrs, "renderer") => { let fn_name = f.sig.ident.to_string(); - items.push(AnalyzeItem { - module: current_mod.to_string(), - item_name: internal_name(&fn_name), - }); + items.push(AnalyzeItem::local( + current_mod.to_string(), + internal_name(&fn_name), + )); } Item::Mod(item_mod) => { if let Some((_, nested)) = &item_mod.content { diff --git a/mingling_pathf/src/type_mapping_builder.rs b/mingling_pathf/src/type_mapping_builder.rs index 0965b47..f6f57a5 100644 --- a/mingling_pathf/src/type_mapping_builder.rs +++ b/mingling_pathf/src/type_mapping_builder.rs @@ -39,7 +39,10 @@ pub fn analyze_and_build_type_mapping_for( }; for ai in analyze_items { - let full_path = if ai.module.is_empty() { + let full_path = if ai.is_foreign { + // Foreign item — use its own module path as-is + format!("{}::{}", ai.module, ai.item_name) + } else if ai.module.is_empty() { format!("{}::{}", module_path, ai.item_name) } else { format!("{}::{}::{}", module_path, ai.module, ai.item_name) 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::*; |
