Skip to main content

tokmd_path/
lib.rs

1//! Single-responsibility path normalization for deterministic matching.
2
3/// Normalize path separators to `/`.
4///
5/// # Examples
6///
7/// ```
8/// use tokmd_path::normalize_slashes;
9///
10/// assert_eq!(normalize_slashes("src\\lib.rs"), "src/lib.rs");
11/// assert_eq!(normalize_slashes("already/forward"), "already/forward");
12/// ```
13///
14/// Mixed separators are all converted:
15///
16/// ```
17/// use tokmd_path::normalize_slashes;
18///
19/// assert_eq!(normalize_slashes("a\\b/c\\d"), "a/b/c/d");
20/// // Already-normalized paths pass through unchanged
21/// assert_eq!(normalize_slashes("no/change"), "no/change");
22/// ```
23#[must_use]
24pub fn normalize_slashes(path: &str) -> String {
25    if path.contains('\\') {
26        path.replace('\\', "/")
27    } else {
28        path.to_string()
29    }
30}
31
32/// Normalize a relative path for matching:
33/// - converts `\` to `/`
34/// - strips all leading `./` segments
35///
36/// # Examples
37///
38/// ```
39/// use tokmd_path::normalize_rel_path;
40///
41/// assert_eq!(normalize_rel_path("./src/main.rs"), "src/main.rs");
42/// assert_eq!(normalize_rel_path(".\\src\\main.rs"), "src/main.rs");
43/// assert_eq!(normalize_rel_path("../lib.rs"), "../lib.rs");
44/// assert_eq!(normalize_rel_path("././src/lib.rs"), "src/lib.rs");
45/// ```
46///
47/// Idempotency — normalizing twice gives the same result:
48///
49/// ```
50/// use tokmd_path::normalize_rel_path;
51///
52/// let once = normalize_rel_path(".\\src\\lib.rs");
53/// let twice = normalize_rel_path(&once);
54/// assert_eq!(once, twice);
55/// assert_eq!(once, "src/lib.rs");
56/// ```
57#[must_use]
58pub fn normalize_rel_path(path: &str) -> String {
59    let normalized = normalize_slashes(path);
60    let mut s = normalized.as_str();
61    while let Some(rest) = s.strip_prefix("./") {
62        s = rest;
63    }
64    s.to_string()
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use proptest::prelude::*;
71
72    #[test]
73    fn normalize_slashes_replaces_backslash() {
74        assert_eq!(normalize_slashes(r"foo\bar\baz.rs"), "foo/bar/baz.rs");
75    }
76
77    #[test]
78    fn normalize_rel_path_strips_dot_slash() {
79        assert_eq!(normalize_rel_path("./src/main.rs"), "src/main.rs");
80    }
81
82    #[test]
83    fn normalize_rel_path_strips_dot_backslash() {
84        assert_eq!(normalize_rel_path(r".\src\main.rs"), "src/main.rs");
85    }
86
87    #[test]
88    fn normalize_rel_path_preserves_non_relative_prefix() {
89        assert_eq!(normalize_rel_path("../src/main.rs"), "../src/main.rs");
90    }
91
92    proptest! {
93        #[test]
94        fn normalize_slashes_no_backslashes(path in "\\PC*") {
95            let normalized = normalize_slashes(&path);
96            prop_assert!(!normalized.contains('\\'));
97        }
98
99        #[test]
100        fn normalize_slashes_idempotent(path in "\\PC*") {
101            let once = normalize_slashes(&path);
102            let twice = normalize_slashes(&once);
103            prop_assert_eq!(once, twice);
104        }
105
106        #[test]
107        fn normalize_rel_path_no_backslashes(path in "\\PC*") {
108            let normalized = normalize_rel_path(&path);
109            prop_assert!(!normalized.contains('\\'));
110        }
111
112        #[test]
113        fn normalize_rel_path_idempotent(path in "\\PC*") {
114            let once = normalize_rel_path(&path);
115            let twice = normalize_rel_path(&once);
116            prop_assert_eq!(once, twice);
117        }
118    }
119}