1#[must_use]
11pub fn module_key(path: &str, module_roots: &[String], module_depth: usize) -> String {
12 let mut p = path.replace('\\', "/");
13 if let Some(stripped) = p.strip_prefix("./") {
14 p = stripped.to_string();
15 }
16 p = p.trim_start_matches('/').to_string();
17
18 module_key_from_normalized(&p, module_roots, module_depth)
19}
20
21#[must_use]
28pub fn module_key_from_normalized(
29 path: &str,
30 module_roots: &[String],
31 module_depth: usize,
32) -> String {
33 let Some((dir_part, _file_part)) = path.rsplit_once('/') else {
34 return "(root)".to_string();
35 };
36
37 let mut dirs = dir_part.split('/').filter(|s| !s.is_empty());
38 let first = match dirs.next() {
39 Some(s) => s,
40 None => return "(root)".to_string(),
41 };
42
43 if !module_roots.iter().any(|r| r == first) {
44 return first.to_string();
45 }
46
47 let depth_needed = module_depth.max(1);
48 let mut key = String::with_capacity(dir_part.len());
49 key.push_str(first);
50
51 for _ in 1..depth_needed {
52 if let Some(seg) = dirs.next() {
53 key.push('/');
54 key.push_str(seg);
55 } else {
56 break;
57 }
58 }
59
60 key
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 #[test]
68 fn module_key_root_level_file() {
69 assert_eq!(module_key("Cargo.toml", &["crates".into()], 2), "(root)");
70 assert_eq!(module_key("./Cargo.toml", &["crates".into()], 2), "(root)");
71 }
72
73 #[test]
74 fn module_key_respects_root_and_depth() {
75 let roots = vec!["crates".into(), "packages".into()];
76 assert_eq!(module_key("crates/foo/src/lib.rs", &roots, 2), "crates/foo");
77 assert_eq!(
78 module_key("packages/bar/src/main.rs", &roots, 2),
79 "packages/bar"
80 );
81 assert_eq!(module_key("crates/foo/src/lib.rs", &roots, 1), "crates");
82 }
83
84 #[test]
85 fn module_key_for_non_root_is_first_directory() {
86 let roots = vec!["crates".into()];
87 assert_eq!(module_key("src/lib.rs", &roots, 2), "src");
88 assert_eq!(module_key("tools/gen.rs", &roots, 2), "tools");
89 }
90
91 #[test]
92 fn module_key_depth_overflow_does_not_include_filename() {
93 let roots = vec!["crates".into()];
94 assert_eq!(module_key("crates/foo.rs", &roots, 2), "crates");
95 assert_eq!(
96 module_key("crates/foo/src/lib.rs", &roots, 10),
97 "crates/foo/src"
98 );
99 }
100
101 #[test]
102 fn module_key_from_normalized_handles_empty_segments() {
103 let roots = vec!["crates".into()];
104 assert_eq!(
105 module_key_from_normalized("crates//foo/src/lib.rs", &roots, 2),
106 "crates/foo"
107 );
108 }
109}