Skip to main content

tldr_cli/
path_validation.rs

1//! Path validation helpers for CLI commands.
2//!
3//! cli-error-clarity-v2 (P2.BUG-4): commands that operate on a project
4//! directory (hubs, impact, whatbreaks, change-impact, …) historically
5//! produced confusing errors when given a regular file:
6//!
7//! - `tldr hubs <file>` → `Error: Path not found: <file>` (false: it exists)
8//! - `tldr change-impact <file>` → `Git: Not a directory (os error 20)`
9//!   (cryptic; the user has no idea what to do)
10//!
11//! These helpers normalise the validation so every directory-taking command
12//! returns the same clear, actionable error mentioning the file path and
13//! suggesting the project root.
14//!
15//! All helpers return `anyhow::Error` so they can be used directly with the
16//! `?` operator inside `run()` methods that already return
17//! `anyhow::Result<()>`.
18
19use std::path::Path;
20
21use anyhow::{bail, Result};
22
23/// Validate that `path` exists and is a directory, producing clear error
24/// messages on failure.
25///
26/// `command` is the CLI subcommand name (e.g. `"hubs"`) used to make the
27/// error message specific.
28pub fn require_directory(path: &Path, command: &str) -> Result<()> {
29    if !path.exists() {
30        bail!("Path not found: {}", path.display());
31    }
32    if path.is_file() {
33        bail!(
34            "{} requires a directory; got file '{}'. Pass the project root \
35             or omit the argument to use the current directory.",
36            command,
37            path.display()
38        );
39    }
40    if !path.is_dir() {
41        bail!(
42            "{} requires a directory; got non-directory path '{}'. Pass the \
43             project root or omit the argument to use the current directory.",
44            command,
45            path.display()
46        );
47    }
48    Ok(())
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54    use std::fs;
55    use tempfile::tempdir;
56
57    #[test]
58    fn directory_passes() {
59        let dir = tempdir().unwrap();
60        require_directory(dir.path(), "hubs").unwrap();
61    }
62
63    #[test]
64    fn file_fails_with_clear_message() {
65        let dir = tempdir().unwrap();
66        let file = dir.path().join("a.py");
67        fs::write(&file, "x = 1\n").unwrap();
68        let err = require_directory(&file, "hubs").unwrap_err().to_string();
69        assert!(err.contains("hubs requires a directory"), "{}", err);
70        assert!(err.contains(file.to_string_lossy().as_ref()), "{}", err);
71    }
72
73    #[test]
74    fn missing_path_fails() {
75        let err = require_directory(Path::new("/no/such/path/xyz"), "hubs")
76            .unwrap_err()
77            .to_string();
78        assert!(err.contains("Path not found"), "{}", err);
79    }
80}