Skip to main content

tokmd_module_key/
lib.rs

1//! Single-responsibility module-key derivation for deterministic grouping.
2
3/// Compute a module key from an input path.
4///
5/// Rules:
6/// - Root-level files become `"(root)"`.
7/// - If the first directory segment is in `module_roots`, include up to
8///   `module_depth` directory segments.
9/// - Otherwise, the module key is the first directory segment.
10#[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/// Compute a module key from a normalized path.
22///
23/// Expected input format:
24/// - forward slashes only
25/// - no leading `./`
26/// - no leading `/`
27#[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}