//! 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 { 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::>().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::>().join("\n"); let result = truncate_diff(&diff); assert!(result.contains("... (50 more lines truncated)")); assert_eq!(result.lines().count(), MAX_DIFF_LINES + 1); } }