107 lines
3.5 KiB
Rust
107 lines
3.5 KiB
Rust
//! Shared helpers for filesystem tools: extracting string arguments from JSON
|
|
//! and producing user-friendly "not found" diagnostics.
|
|
|
|
use std::path::Path;
|
|
use serde_json::Value;
|
|
use anyhow::{Result, anyhow};
|
|
|
|
/// Extract a required string argument from a JSON args map.
|
|
///
|
|
/// Return: the value as `String` if present and a string type; `Err` if missing
|
|
/// or of a different JSON type (null, number, boolean, array, object).
|
|
pub fn arg_str(args: &Value, name: &str) -> Result<String> {
|
|
args.get(name)
|
|
.and_then(|v| v.as_str())
|
|
.map(std::string::ToString::to_string)
|
|
.ok_or_else(|| anyhow!("missing required argument: {name}"))
|
|
}
|
|
|
|
/// Produce a user-friendly diagnostic string when a path doesn't resolve or exist.
|
|
///
|
|
/// Checks whether the resolved path canonically falls inside any workspace root
|
|
/// and reports either "path outside workspaces" or "path does not exist" accordingly.
|
|
///
|
|
/// Return: a one-line description of the resolution failure.
|
|
pub fn not_found_help(ctx: &super::super::ToolCtx, path: &Path, rel: &str) -> String {
|
|
let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
|
|
let in_ws = ctx.workspaces.iter().any(|w| {
|
|
let wc = w.canonicalize().unwrap_or_else(|_| w.clone());
|
|
canon.starts_with(&wc)
|
|
});
|
|
if in_ws {
|
|
format!("path '{}' does not exist (resolved to {})", rel, canon.display())
|
|
} else {
|
|
format!(
|
|
"path '{}' is outside all workspace roots. Workspace roots: {}",
|
|
rel,
|
|
ctx.workspaces.iter().map(|w| w.display().to_string()).collect::<Vec<_>>().join(", ")
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Maximum number of lines a diff block may contain before being truncated.
|
|
pub const MAX_DIFF_LINES: usize = 200;
|
|
|
|
/// Cap a unified diff at `MAX_DIFF_LINES` lines, appending a truncation note.
|
|
///
|
|
/// Return: `diff` unchanged if it's within the limit; otherwise the first
|
|
/// `MAX_DIFF_LINES` lines followed by `"... ({N} more lines truncated)"`.
|
|
pub fn truncate_diff(diff: &str) -> String {
|
|
let lines: Vec<&str> = diff.lines().collect();
|
|
if lines.len() <= MAX_DIFF_LINES {
|
|
return diff.to_string();
|
|
}
|
|
let remaining = lines.len() - MAX_DIFF_LINES;
|
|
format!("{}\n... ({remaining} more lines truncated)", lines[..MAX_DIFF_LINES].join("\n"))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn test_arg_str_found() {
|
|
let args = json!({"key": "value"});
|
|
assert_eq!(arg_str(&args, "key").unwrap(), "value");
|
|
}
|
|
|
|
#[test]
|
|
fn test_arg_str_missing() {
|
|
let args = json!({"other": "value"});
|
|
assert!(arg_str(&args, "key").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_arg_str_empty_string() {
|
|
let args = json!({"key": ""});
|
|
assert_eq!(arg_str(&args, "key").unwrap(), "");
|
|
}
|
|
|
|
#[test]
|
|
fn test_arg_str_wrong_type() {
|
|
let args = json!({"key": 42});
|
|
assert!(arg_str(&args, "key").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_arg_str_null() {
|
|
let args = json!({"key": null});
|
|
assert!(arg_str(&args, "key").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_truncate_diff_under_limit_unchanged() {
|
|
let diff = "line1\nline2\nline3";
|
|
assert_eq!(truncate_diff(diff), diff);
|
|
}
|
|
|
|
#[test]
|
|
fn test_truncate_diff_over_limit_truncates() {
|
|
let diff = (0..250).map(|i| format!("line{i}")).collect::<Vec<_>>().join("\n");
|
|
let result = truncate_diff(&diff);
|
|
assert!(result.contains("... (50 more lines truncated)"));
|
|
assert_eq!(result.lines().count(), MAX_DIFF_LINES + 1);
|
|
}
|
|
}
|