1pub 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}