Skip to main content

graphify_core/
id.rs

1/// Generate a deterministic ID from string parts.
2///
3/// Matches the Python `_make_id` implementation exactly:
4/// ```python
5/// def _make_id(*parts: str) -> str:
6///     combined = "_".join(p.strip("_.") for p in parts if p)
7///     cleaned = re.sub(r"[^a-zA-Z0-9]+", "_", combined)
8///     return cleaned.strip("_").lower()
9/// ```
10pub fn make_id(parts: &[&str]) -> String {
11    let combined = parts
12        .iter()
13        .filter(|p| !p.is_empty())
14        .map(|p| p.trim_matches(&['_', '.'][..]))
15        .collect::<Vec<_>>()
16        .join("_");
17
18    let mut cleaned = String::with_capacity(combined.len());
19    let mut prev_was_sep = false;
20    for ch in combined.chars() {
21        if ch.is_alphanumeric() {
22            cleaned.push(ch);
23            prev_was_sep = false;
24        } else if !prev_was_sep {
25            cleaned.push('_');
26            prev_was_sep = true;
27        }
28    }
29
30    cleaned.trim_matches('_').to_lowercase()
31}
32
33#[cfg(test)]
34mod tests {
35    use super::*;
36
37    #[test]
38    fn basic_parts() {
39        assert_eq!(make_id(&["Hello", "World"]), "hello_world");
40    }
41
42    #[test]
43    fn strips_dots_and_underscores() {
44        assert_eq!(make_id(&["__foo__", "..bar.."]), "foo_bar");
45    }
46
47    #[test]
48    fn replaces_special_chars() {
49        assert_eq!(make_id(&["my-class", "method()"]), "my_class_method");
50    }
51
52    #[test]
53    fn filters_empty_parts() {
54        assert_eq!(make_id(&["a", "", "b", ""]), "a_b");
55    }
56
57    #[test]
58    fn single_part() {
59        assert_eq!(make_id(&["SomeClass"]), "someclass");
60    }
61
62    #[test]
63    fn all_empty() {
64        assert_eq!(make_id(&["", ""]), "");
65    }
66
67    #[test]
68    fn special_only() {
69        assert_eq!(make_id(&["---"]), "");
70    }
71
72    #[test]
73    fn mixed_unicode_and_ascii() {
74        assert_eq!(make_id(&["foo::bar"]), "foo_bar");
75    }
76
77    #[test]
78    fn consecutive_separators_collapsed() {
79        assert_eq!(make_id(&["a!!!b"]), "a_b");
80    }
81
82    #[test]
83    fn python_compat_complex() {
84        assert_eq!(make_id(&["__init__", "MyClass"]), "init_myclass");
85    }
86
87    #[test]
88    fn cjk_identifiers_preserved() {
89        assert_eq!(make_id(&["类名"]), "类名");
90        assert_eq!(make_id(&["関数", "Helper"]), "関数_helper");
91        assert_eq!(make_id(&["모듈", "클래스"]), "모듈_클래스");
92    }
93
94    #[test]
95    fn mixed_cjk_and_special_chars() {
96        assert_eq!(make_id(&["类名::方法"]), "类名_方法");
97        assert_eq!(make_id(&["my-类"]), "my_类");
98    }
99}