halo/
field_mapper.rs

1//! Field mapper:把 Rust 字段名映射为列名(对齐 go-sqlbuilder `fieldmapper.go`)。
2
3use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
4
5/// 字段名映射函数类型(对齐 go 的 `FieldMapperFunc`)。
6pub type FieldMapperFunc = Arc<dyn Fn(&str) -> String + Send + Sync + 'static>;
7
8fn identity_impl(s: &str) -> String {
9    s.to_string()
10}
11
12static IDENTITY_MAPPER: OnceLock<FieldMapperFunc> = OnceLock::new();
13
14/// 恒等 mapper(等价于 go 的 `DefaultFieldMapper == nil`)。
15pub fn identity_mapper() -> FieldMapperFunc {
16    IDENTITY_MAPPER
17        .get_or_init(|| Arc::new(identity_impl))
18        .clone()
19}
20
21static DEFAULT_FIELD_MAPPER: OnceLock<Mutex<FieldMapperFunc>> = OnceLock::new();
22static DEFAULT_FIELD_MAPPER_LOCK: Mutex<()> = Mutex::new(());
23
24fn mapper_cell() -> &'static Mutex<FieldMapperFunc> {
25    DEFAULT_FIELD_MAPPER.get_or_init(|| Mutex::new(identity_mapper()))
26}
27
28/// 获取当前全局默认 FieldMapper(对齐 go 的 `DefaultFieldMapper`)。
29pub fn default_field_mapper() -> FieldMapperFunc {
30    mapper_cell()
31        .lock()
32        .unwrap_or_else(|e| e.into_inner())
33        .clone()
34}
35
36/// 设置全局默认 FieldMapper,返回旧值。
37pub fn set_default_field_mapper(mapper: FieldMapperFunc) -> FieldMapperFunc {
38    let mut g = mapper_cell().lock().unwrap_or_else(|e| e.into_inner());
39    std::mem::replace(&mut *g, mapper)
40}
41
42/// 修改全局默认 FieldMapper 的 RAII guard(会持有一个全局锁,避免并行测试互相干扰)。
43pub struct DefaultFieldMapperGuard {
44    _lock: MutexGuard<'static, ()>,
45    old: FieldMapperFunc,
46}
47
48impl Drop for DefaultFieldMapperGuard {
49    fn drop(&mut self) {
50        let _ = set_default_field_mapper(self.old.clone());
51    }
52}
53
54/// 在一个作用域内临时设置默认 FieldMapper,并保证退出作用域后自动恢复。
55pub fn set_default_field_mapper_scoped(mapper: FieldMapperFunc) -> DefaultFieldMapperGuard {
56    let lock = DEFAULT_FIELD_MAPPER_LOCK
57        .lock()
58        .unwrap_or_else(|e| e.into_inner());
59    let old = set_default_field_mapper(mapper);
60    DefaultFieldMapperGuard { _lock: lock, old }
61}
62
63fn convert_with_separator(s: &str, sep: char) -> String {
64    let mut out = String::with_capacity(s.len() + 8);
65    let mut prev: Option<char> = None;
66    let chars: Vec<char> = s.chars().collect();
67
68    for (i, &c) in chars.iter().enumerate() {
69        let next = chars.get(i + 1).copied();
70        let is_upper = c.is_ascii_uppercase();
71
72        if is_upper {
73            if let Some(p) = prev {
74                let prev_is_lower_or_digit = p.is_ascii_lowercase() || p.is_ascii_digit();
75                let prev_is_upper = p.is_ascii_uppercase();
76                let next_is_lower = next.map(|n| n.is_ascii_lowercase()).unwrap_or(false);
77
78                if prev_is_lower_or_digit || (prev_is_upper && next_is_lower) {
79                    out.push(sep);
80                }
81            }
82            out.push(c.to_ascii_lowercase());
83        } else {
84            out.push(c);
85        }
86
87        prev = Some(c);
88    }
89
90    out
91}
92
93/// SnakeCaseMapper:将 `CamelCase` 转为 `snake_case`(对齐 go 的 `SnakeCaseMapper`)。
94///
95/// 注意:go-sqlbuilder 依赖 `xstrings.ToSnakeCase`。这里实现的是足以覆盖本仓库测试的规则子集:
96/// - 大写转小写
97/// - 单词边界插入 `_`(`aB`/`a1B`/`ABc` 等)
98pub fn snake_case_mapper(s: &str) -> String {
99    convert_with_separator(s, '_')
100}
101
102/// KebabcaseMapper:将 `CamelCase` 转为 `kebab-case`(用于测试更多转换策略)。
103pub fn kebab_case_mapper(s: &str) -> String {
104    convert_with_separator(s, '-')
105}
106
107/// UpperCaseMapper:将字段名转为全部大写。
108pub fn upper_case_mapper(s: &str) -> String {
109    s.to_ascii_uppercase()
110}
111
112/// PrefixMapper:返回一个在字段名前添加固定前缀的 mapper。
113pub fn prefix_mapper(prefix: &'static str) -> FieldMapperFunc {
114    Arc::new(move |name| format!("{prefix}{name}"))
115}
116
117/// SuffixMapper:返回一个在字段名后添加固定后缀的 mapper。
118pub fn suffix_mapper(suffix: &'static str) -> FieldMapperFunc {
119    Arc::new(move |name| format!("{name}{suffix}"))
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn camel_case_helpers_work() {
128        assert_eq!(snake_case_mapper("FieldName"), "field_name");
129        assert_eq!(kebab_case_mapper("FieldName"), "field-name");
130    }
131
132    #[test]
133    fn upper_case_mapper_changes_case() {
134        assert_eq!(upper_case_mapper("FieldName"), "FIELDNAME");
135    }
136
137    #[test]
138    fn prefix_suffix_mappers_apply() {
139        let prefix = prefix_mapper("db_");
140        let suffix = suffix_mapper("_col");
141        assert_eq!(prefix("FieldName"), "db_FieldName");
142        assert_eq!(suffix("FieldName"), "FieldName_col");
143    }
144}