Skip to main content

depguard_types/
path.rs

1use camino::{Utf8Path, Utf8PathBuf};
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4
5/// Canonical repo-relative path used in findings and reports.
6///
7/// Normalization rules are intentionally simple and deterministic:
8/// - always forward slashes (`/`)
9/// - no leading `./`
10/// - never absolute (best-effort: absolute inputs are preserved but flagged by checks)
11#[derive(
12    Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
13)]
14#[serde(transparent)]
15pub struct RepoPath(String);
16
17impl Default for RepoPath {
18    fn default() -> Self {
19        RepoPath::new(".")
20    }
21}
22
23impl RepoPath {
24    pub fn new<S: AsRef<str>>(s: S) -> Self {
25        let mut v = s.as_ref().replace('\\', "/");
26        while v.starts_with("./") {
27            v = v.trim_start_matches("./").to_string();
28        }
29        // Avoid empty path; keep it explicit.
30        if v.is_empty() {
31            v = ".".to_string();
32        }
33        Self(v)
34    }
35
36    pub fn as_str(&self) -> &str {
37        &self.0
38    }
39
40    pub fn to_utf8_pathbuf(&self) -> Utf8PathBuf {
41        Utf8PathBuf::from(self.0.clone())
42    }
43
44    pub fn join(&self, segment: &str) -> RepoPath {
45        let base = Utf8Path::new(self.as_str());
46        RepoPath::new(base.join(segment).as_str())
47    }
48}
49
50impl From<&Utf8Path> for RepoPath {
51    fn from(value: &Utf8Path) -> Self {
52        RepoPath::new(value.as_str())
53    }
54}
55
56impl From<Utf8PathBuf> for RepoPath {
57    fn from(value: Utf8PathBuf) -> Self {
58        RepoPath::new(value.as_str())
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn repo_path_normalizes_separators_and_prefix() {
68        let path = RepoPath::new(r".\crates\depguard\Cargo.toml");
69        assert_eq!(path.as_str(), "crates/depguard/Cargo.toml");
70
71        let path = RepoPath::new("././Cargo.toml");
72        assert_eq!(path.as_str(), "Cargo.toml");
73    }
74
75    #[test]
76    fn repo_path_empty_defaults_to_dot() {
77        let path = RepoPath::new("");
78        assert_eq!(path.as_str(), ".");
79
80        let path = RepoPath::default();
81        assert_eq!(path.as_str(), ".");
82    }
83
84    #[test]
85    fn repo_path_join_uses_forward_slashes() {
86        let base = RepoPath::new("crates/depguard");
87        let joined = base.join("src/lib.rs");
88        assert_eq!(joined.as_str(), "crates/depguard/src/lib.rs");
89    }
90
91    #[test]
92    fn repo_path_from_utf8_pathbuf() {
93        let buf = Utf8PathBuf::from("crates/depguard");
94        let path = RepoPath::from(buf);
95        assert_eq!(path.as_str(), "crates/depguard");
96    }
97
98    #[test]
99    fn repo_path_to_utf8_pathbuf() {
100        let path = RepoPath::new("crates/depguard");
101        let buf = path.to_utf8_pathbuf();
102        assert_eq!(buf.as_str(), "crates/depguard");
103    }
104}