Skip to main content

rust_store_core/naming/
canonical.rs

1//! canonical 归一与三种重组(设计 §6.2;**单点实现**,绑定只转发)。
2
3/// 词分隔符(设计 §6.2):`_` / `-` / `.` / 空格。
4#[inline]
5fn is_sep(c: char) -> bool {
6    matches!(c, '_' | '-' | '.' | ' ')
7}
8
9fn flush(cur: &mut String, out: &mut Vec<String>) {
10    if !cur.is_empty() {
11        out.push(std::mem::take(cur).to_lowercase());
12    }
13}
14
15/// 用户任意写法 → token 序列(**全小写**)。
16///
17/// 规则(设计 §6.2、V7):
18/// - 分隔符 `_`/`-`/`.`/空格 处切分;其它非 ASCII 字母数字字符**一律按分隔符**处理;
19/// - 小写或数字 → 大写处切分(`orderTotal` → `[order, total]`);
20/// - 连续大写后跟小写时,**最后一个大写归下一段**(`HTTPServer` → `[http, server]`、
21///   `userID` → `[user, id]`);
22/// - **数字视为词内字符、不单独成词**(`order2Items` → `[order2, items]`)。
23pub fn canonical(s: &str) -> Vec<String> {
24    let chars: Vec<char> = s.chars().collect();
25    let mut out: Vec<String> = Vec::new();
26    let mut cur = String::new();
27    for i in 0..chars.len() {
28        let c = chars[i];
29        if is_sep(c) || !c.is_ascii_alphanumeric() {
30            flush(&mut cur, &mut out);
31            continue;
32        }
33        if cur.is_empty() {
34            cur.push(c);
35            continue;
36        }
37        let prev = cur.chars().last().unwrap();
38        if c.is_ascii_uppercase() {
39            let prev_lower_or_digit = prev.is_ascii_lowercase() || prev.is_ascii_digit();
40            let next_lower = chars
41                .get(i + 1)
42                .map(|n| n.is_ascii_lowercase())
43                .unwrap_or(false);
44            // 前一枚为小写/数字 → 切;连续大写串的最后一枚大写且其后为小写 → 切
45            if prev_lower_or_digit || (prev.is_ascii_uppercase() && next_lower) {
46                flush(&mut cur, &mut out);
47            }
48            cur.push(c);
49        } else {
50            cur.push(c);
51        }
52    }
53    flush(&mut cur, &mut out);
54    out
55}
56
57fn cap(s: &str) -> String {
58    let mut it = s.chars();
59    match it.next() {
60        Some(first) => first.to_ascii_uppercase().to_string() + it.as_str(),
61        None => String::new(),
62    }
63}
64
65/// token 序列 → snake_case(`t1_t2`)
66pub fn to_snake(tokens: &[String]) -> String {
67    tokens.join("_")
68}
69
70/// token 序列 → camelCase(`t1` + `T2`)
71pub fn to_camel(tokens: &[String]) -> String {
72    let mut out = String::new();
73    for (i, t) in tokens.iter().enumerate() {
74        if i == 0 {
75            out.push_str(t);
76        } else {
77            out.push_str(&cap(t));
78        }
79    }
80    out
81}
82
83/// token 序列 → PascalCase(`T1T2`)
84pub fn to_pascal(tokens: &[String]) -> String {
85    tokens.iter().map(|t| cap(t)).collect()
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    fn v(xs: &[&str]) -> Vec<String> {
93        xs.iter().map(|s| s.to_string()).collect()
94    }
95
96    /// 设计 §6.2 全部示例向量(V7 含数字词内字符)
97    #[test]
98    fn canonical_design_vectors() {
99        assert_eq!(canonical("orderTotal"), v(&["order", "total"]));
100        assert_eq!(canonical("order_total"), v(&["order", "total"]));
101        assert_eq!(canonical("Order.Total"), v(&["order", "total"]));
102        assert_eq!(canonical("order2Items"), v(&["order2", "items"]));
103        assert_eq!(canonical("HTTPServer"), v(&["http", "server"]));
104        assert_eq!(canonical("userID"), v(&["user", "id"]));
105    }
106
107    #[test]
108    fn canonical_edges() {
109        assert_eq!(canonical(""), v(&[]));
110        assert_eq!(canonical("order-Total"), v(&["order", "total"]));
111        assert_eq!(canonical("order  total"), v(&["order", "total"])); // 连续分隔符折叠
112        assert_eq!(canonical("_id"), v(&["id"])); // 见 §8 注意 2:调用方不得对 _id 翻译
113        assert_eq!(canonical("HTTP"), v(&["http"])); // 全大写无后随小写
114    }
115
116    /// 重组三式(设计 §6.2)
117    #[test]
118    fn reassemble_vectors() {
119        let t = v(&["order", "total"]);
120        assert_eq!(to_snake(&t), "order_total");
121        assert_eq!(to_camel(&t), "orderTotal");
122        assert_eq!(to_pascal(&t), "OrderTotal");
123    }
124
125    /// 可逆性:三式重组结果再归一须回到同一 token 序列
126    #[test]
127    fn roundtrip_stable() {
128        for s in [
129            "orderTotal",
130            "order_total",
131            "Order.Total",
132            "userID",
133            "HTTPServer",
134        ] {
135            let t = canonical(s);
136            assert_eq!(canonical(&to_snake(&t)), t);
137            assert_eq!(canonical(&to_camel(&t)), t);
138            assert_eq!(canonical(&to_pascal(&t)), t);
139        }
140    }
141}