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}