Skip to main content

alun_utils/
str.rs

1//! 字符串工具:驼峰/蛇形互转、截断、判空、随机串等
2
3/// 字符串扩展 trait(为 `&str` 添加实用方法)
4///
5/// 导入后可对任何字符串切片调用驼峰/蛇形互转、截断、判空、随机串等方法。
6pub trait StrExt {
7    /// 是否为空白(仅含空格/制表符/换行符)
8    fn is_blank(&self) -> bool;
9    /// 蛇形命名 → 驼峰命名(如 `user_name` → `userName`)
10    fn to_camel(&self) -> String;
11    /// 驼峰命名 → 蛇形命名(如 `UserName` → `user_name`)
12    fn to_snake(&self) -> String;
13    /// 按字符数截断(超出末尾补 `...`)
14    fn truncate(&self, max: usize) -> String;
15    /// 生成指定长度的随机字母数字串
16    fn random(len: usize) -> String;
17    /// 非空白字符(`is_blank` 的取反)
18    fn has_text(&self) -> bool { !self.is_blank() }
19}
20
21impl StrExt for str {
22    fn is_blank(&self) -> bool { self.trim().is_empty() }
23
24    fn to_camel(&self) -> String {
25        self.split('_')
26            .enumerate()
27            .map(|(i, w)| {
28                if i == 0 { w.to_lowercase() }
29                else { let mut c = w.chars(); c.next().map(|x| x.to_uppercase().chain(c).collect()).unwrap_or_default() }
30            })
31            .collect()
32    }
33
34    fn to_snake(&self) -> String {
35        let mut result = String::with_capacity(self.len() + 4);
36        for (i, ch) in self.chars().enumerate() {
37            if ch.is_uppercase() && i > 0 {
38                result.push('_');
39            }
40            result.push(ch.to_ascii_lowercase());
41        }
42        result
43    }
44
45    fn truncate(&self, max: usize) -> String {
46        if self.len() <= max { self.to_string() }
47        else { format!("{}...", &self[..max]) }
48    }
49
50    fn random(len: usize) -> String {
51        use rand::Rng;
52        const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789";
53        let mut rng = rand::thread_rng();
54        (0..len).map(|_| CHARSET[rng.gen_range(0..CHARSET.len())] as char).collect()
55    }
56}
57
58/// 清理文件名 —— 将非字母/数字/点/横线/下划线的字符替换为 `_`
59pub fn sanitize_filename(filename: &str) -> String {
60    use regex::Regex;
61    let re = Regex::new(r"[^a-zA-Z0-9.\-_]").unwrap();
62    re.replace_all(filename, "_").to_string()
63}
64
65/// 解析 JSON 字符串为 `serde_json::Value`
66pub fn parse_json_value(value: &str) -> Result<serde_json::Value, serde_json::Error> {
67    serde_json::from_str(value)
68}
69
70/// 格式化文件大小(字节 → 人类可读)
71///
72/// # 示例
73///
74/// ```
75/// assert_eq!(alun_utils::str::format_file_size(0), "0 B");
76/// assert_eq!(alun_utils::str::format_file_size(1024), "1.00 KB");
77/// assert_eq!(alun_utils::str::format_file_size(1_500_000), "1.43 MB");
78/// ```
79pub fn format_file_size(bytes: u64) -> String {
80    const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"];
81
82    if bytes == 0 {
83        return "0 B".to_string();
84    }
85
86    let i = (bytes as f64).log(1024.0).floor() as i32;
87    let size = bytes as f64 / 1024_f64.powi(i);
88    let unit = UNITS.get(i as usize).unwrap_or(&"B");
89
90    format!("{:.2} {}", size, unit)
91}
92
93/// 清理字符串参数 —— 去除前后空格
94pub fn clean_string_param(s: &str) -> String {
95    s.trim().to_string()
96}
97
98/// 清理邮箱参数 —— 去除前后空格,转为小写
99pub fn clean_email(email: &str) -> String {
100    email.trim().to_lowercase()
101}
102
103/// 清理密码参数 —— 只去除前后空格,保留中间空格
104pub fn clean_password(password: &str) -> String {
105    password.trim().to_string()
106}
107
108/// 用户输入清理器 —— 提供注册/登录请求参数的规范化清理
109pub struct InputCleaner;
110
111impl InputCleaner {
112    /// 清理注册请求:邮箱小写去空格、密码去空格、昵称去空格
113    ///
114    /// 返回 `(email, password, nickname)` 三元组。
115    pub fn clean_register_input(
116        email: &str,
117        password: &str,
118        nickname: &str,
119    ) -> (String, String, String) {
120        let email = clean_email(email);
121        let password = clean_password(password);
122        let nickname = clean_string_param(nickname);
123        (email, password, nickname)
124    }
125
126    /// 清理登录请求:邮箱小写去空格、密码去空格
127    ///
128    /// 返回 `(email, password)` 二元组。
129    pub fn clean_login_input(email: &str, password: &str) -> (String, String) {
130        let email = clean_email(email);
131        let password = clean_password(password);
132        (email, password)
133    }
134}
135
136/// 生成邀请码 —— 12 位随机字母数字串
137pub fn generate_invite_code() -> String {
138    use rand::{Rng, distributions::Alphanumeric};
139    rand::thread_rng()
140        .sample_iter(&Alphanumeric)
141        .take(12)
142        .map(char::from)
143        .collect()
144}
145
146/// 生成指定位数的不含数字 `0` 的随机数字串
147///
148/// # 参数
149///
150/// * `n` - 位数(若为 0 则返回空字符串)
151///
152/// # 示例
153///
154/// ```
155/// let s = alun_utils::str::generate_random_digits(6);
156/// assert_eq!(s.len(), 6);
157/// assert!(!s.chars().any(|c| c == '0'));
158/// ```
159pub fn generate_random_digits(n: usize) -> String {
160    if n == 0 {
161        return String::new();
162    }
163    use rand::Rng;
164    let mut rng = rand::thread_rng();
165    (0..n)
166        .map(|_| (rng.gen_range(1..=9) + b'0') as char)
167        .collect()
168}
169
170/// 生成由大小写字母和数字(不含 `0` 和 `O`/`I`/`l` 等易混淆字符)组成的随机字符串
171///
172/// # 参数
173///
174/// * `length` - 字符串长度(若为 0 则返回空字符串)
175///
176/// # 示例
177///
178/// ```
179/// let s = alun_utils::str::generate_random_alphanum(8);
180/// assert_eq!(s.len(), 8);
181/// assert!(!s.chars().any(|c| c == '0' || c == 'O' || c == 'I' || c == 'l'));
182/// ```
183pub fn generate_random_alphanum(length: usize) -> String {
184    const CHARSET: &[u8] = b"ABCDEFGHJKMNPQRSTUVWXYZ\
185                              abcdefghjkmnpqrstuvwxyz\
186                              123456789";
187    use rand::Rng;
188    let mut rng = rand::thread_rng();
189    let mut result = String::with_capacity(length);
190    for _ in 0..length {
191        let idx = rng.gen_range(0..CHARSET.len());
192        result.push(CHARSET[idx] as char);
193    }
194    result
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_camel() { assert_eq!("user_name".to_camel(), "userName"); }
203    #[test]
204    fn test_snake() { assert_eq!("UserName".to_snake(), "user_name"); }
205    #[test]
206    fn test_blank() { assert!("  ".is_blank()); assert!(!"abc".is_blank()); }
207}