Skip to main content

cool_core/util/
mod.rs

1//! 工具模块
2//!
3//! 对应 TypeScript 版本的 `util/`
4
5use md5::{Digest, Md5};
6use uuid::Uuid;
7
8/// 生成 UUID v4
9pub fn uuid() -> String {
10    Uuid::new_v4().to_string()
11}
12
13/// 生成不带横杠的 UUID
14pub fn uuid_simple() -> String {
15    Uuid::new_v4().simple().to_string()
16}
17
18/// MD5 加密
19pub fn md5<S: AsRef<[u8]>>(data: S) -> String {
20    let mut hasher = Md5::new();
21    hasher.update(data);
22    let result = hasher.finalize();
23    format!("{:x}", result)
24}
25
26/// 生成随机字符串
27pub fn random_string(len: usize) -> String {
28    use rand::Rng;
29    const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
30    let mut rng = rand::rng();
31    (0..len)
32        .map(|_| {
33            let idx = rng.random_range(0..CHARSET.len());
34            CHARSET[idx] as char
35        })
36        .collect()
37}
38
39/// 生成随机数字字符串
40pub fn random_number_string(len: usize) -> String {
41    use rand::Rng;
42    let mut rng = rand::rng();
43    (0..len)
44        .map(|_| rng.random_range(0..10).to_string())
45        .collect()
46}
47
48/// 驼峰转下划线
49pub fn camel_to_snake(s: &str) -> String {
50    let mut result = String::new();
51    for (i, c) in s.chars().enumerate() {
52        if c.is_uppercase() {
53            if i > 0 {
54                result.push('_');
55            }
56            result.push(c.to_lowercase().next().unwrap());
57        } else {
58            result.push(c);
59        }
60    }
61    result
62}
63
64/// 下划线转驼峰
65pub fn snake_to_camel(s: &str) -> String {
66    let mut result = String::new();
67    let mut capitalize_next = false;
68
69    for c in s.chars() {
70        if c == '_' {
71            capitalize_next = true;
72        } else if capitalize_next {
73            result.push(c.to_uppercase().next().unwrap());
74            capitalize_next = false;
75        } else {
76            result.push(c);
77        }
78    }
79    result
80}
81
82/// 首字母大写
83pub fn capitalize(s: &str) -> String {
84    let mut chars = s.chars();
85    match chars.next() {
86        None => String::new(),
87        Some(first) => first.to_uppercase().chain(chars).collect(),
88    }
89}
90
91/// 首字母小写
92pub fn uncapitalize(s: &str) -> String {
93    let mut chars = s.chars();
94    match chars.next() {
95        None => String::new(),
96        Some(first) => first.to_lowercase().chain(chars).collect(),
97    }
98}
99
100/// 获取当前时间戳(秒)
101pub fn timestamp() -> i64 {
102    chrono::Utc::now().timestamp()
103}
104
105/// 获取当前时间戳(毫秒)
106pub fn timestamp_millis() -> i64 {
107    chrono::Utc::now().timestamp_millis()
108}
109
110/// 格式化日期时间
111pub fn format_datetime(dt: chrono::DateTime<chrono::Utc>, format: &str) -> String {
112    dt.format(format).to_string()
113}
114
115/// 解析日期时间
116pub fn parse_datetime(s: &str, format: &str) -> Option<chrono::DateTime<chrono::Utc>> {
117    chrono::NaiveDateTime::parse_from_str(s, format)
118        .ok()
119        .and_then(|dt| dt.and_local_timezone(chrono::Utc).single())
120}
121
122/// IP 地址工具
123pub mod ip {
124    /// 检查是否是内网 IP
125    pub fn is_private(ip: &str) -> bool {
126        if let Ok(addr) = ip.parse::<std::net::IpAddr>() {
127            match addr {
128                std::net::IpAddr::V4(v4) => {
129                    v4.is_private() || v4.is_loopback() || v4.is_link_local()
130                }
131                std::net::IpAddr::V6(v6) => v6.is_loopback(),
132            }
133        } else {
134            false
135        }
136    }
137
138    /// 获取本机 IP
139    pub fn local_ip() -> Option<String> {
140        local_ip_address::local_ip().ok().map(|ip| ip.to_string())
141    }
142}
143
144/// 字符串工具
145pub mod string {
146    /// 是否为空或空白
147    pub fn is_blank(s: &str) -> bool {
148        s.trim().is_empty()
149    }
150
151    /// 是否不为空且不为空白
152    pub fn is_not_blank(s: &str) -> bool {
153        !is_blank(s)
154    }
155
156    /// 截取字符串
157    pub fn truncate(s: &str, max_len: usize) -> &str {
158        if s.len() <= max_len {
159            s
160        } else {
161            &s[..max_len]
162        }
163    }
164
165    /// 移除 HTML 标签
166    pub fn strip_html(s: &str) -> String {
167        let re = regex::Regex::new(r"<[^>]*>").unwrap();
168        re.replace_all(s, "").to_string()
169    }
170
171    /// 替换所有匹配的字符串
172    ///
173    /// 对应 TypeScript 版本的 `String.prototype.replaceAll`
174    pub fn replace_all(s: &str, from: &str, to: &str) -> String {
175        s.replace(from, to)
176    }
177
178    /// 首字母大写
179    pub fn capitalize(s: &str) -> String {
180        let mut chars = s.chars();
181        match chars.next() {
182            None => String::new(),
183            Some(first) => first.to_uppercase().chain(chars).collect(),
184        }
185    }
186
187    /// 首字母小写
188    pub fn uncapitalize(s: &str) -> String {
189        let mut chars = s.chars();
190        match chars.next() {
191            None => String::new(),
192            Some(first) => first.to_lowercase().chain(chars).collect(),
193        }
194    }
195}
196
197/// 位置工具
198///
199/// 对应 TypeScript 版本的 `util/location.ts`
200pub mod location {
201    use std::env;
202    use std::path::{Path, PathBuf};
203
204    /// 获取当前可执行文件的目录
205    pub fn get_executable_dir() -> Option<PathBuf> {
206        env::current_exe()
207            .ok()
208            .and_then(|exe| exe.parent().map(|p| p.to_path_buf()))
209    }
210
211    /// 获取当前工作目录
212    pub fn get_current_dir() -> Option<PathBuf> {
213        env::current_dir().ok()
214    }
215
216    /// 获取项目根目录(查找 Cargo.toml 或 package.json)
217    pub fn get_project_root() -> Option<PathBuf> {
218        let mut current = env::current_dir().ok()?;
219
220        loop {
221            // 检查是否存在 Cargo.toml(Rust 项目)
222            if current.join("Cargo.toml").exists() {
223                return Some(current);
224            }
225
226            // 检查是否存在 package.json(Node.js 项目)
227            if current.join("package.json").exists() {
228                return Some(current);
229            }
230
231            // 检查是否存在 dist 目录(编译后的目录)
232            if current.join("dist").exists() {
233                return Some(current);
234            }
235
236            // 向上查找
237            match current.parent() {
238                Some(parent) => current = parent.to_path_buf(),
239                None => break,
240            }
241        }
242
243        None
244    }
245
246    /// 获取编译后的文件路径(dist 目录)
247    ///
248    /// 对应 TypeScript 版本的 `getDistPath()`
249    pub fn get_dist_path() -> Option<PathBuf> {
250        get_project_root().map(|root| root.join("dist"))
251    }
252
253    /// 规范化路径
254    pub fn normalize_path(path: &Path) -> PathBuf {
255        path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
256    }
257
258    /// 连接路径
259    pub fn join_path<P: AsRef<Path>>(base: &Path, parts: &[P]) -> PathBuf {
260        let mut result = base.to_path_buf();
261        for part in parts {
262            result = result.join(part);
263        }
264        result
265    }
266
267    /// 获取相对路径
268    ///
269    /// 注意:这是一个简化实现,如果需要完整功能,需要添加 pathdiff 依赖
270    pub fn relative_path(_from: &Path, _to: &Path) -> Option<PathBuf> {
271        // 简化实现:如果需要完整功能,可以添加 pathdiff 依赖
272        // pathdiff::diff_paths(to, from)
273        None
274    }
275
276    /// 检查路径是否存在
277    pub fn path_exists(path: &Path) -> bool {
278        path.exists()
279    }
280
281    /// 检查是否是文件
282    pub fn is_file(path: &Path) -> bool {
283        path.is_file()
284    }
285
286    /// 检查是否是目录
287    pub fn is_dir(path: &Path) -> bool {
288        path.is_dir()
289    }
290
291    /// 获取文件扩展名
292    pub fn get_extension(path: &Path) -> Option<String> {
293        path.extension()
294            .and_then(|ext| ext.to_str())
295            .map(|s| s.to_string())
296    }
297
298    /// 获取文件名(不含扩展名)
299    pub fn get_stem(path: &Path) -> Option<String> {
300        path.file_stem()
301            .and_then(|stem| stem.to_str())
302            .map(|s| s.to_string())
303    }
304
305    /// 获取文件名(含扩展名)
306    pub fn get_filename(path: &Path) -> Option<String> {
307        path.file_name()
308            .and_then(|name| name.to_str())
309            .map(|s| s.to_string())
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    #[test]
318    fn test_camel_to_snake() {
319        assert_eq!(camel_to_snake("userName"), "user_name");
320        assert_eq!(camel_to_snake("UserName"), "user_name");
321        assert_eq!(camel_to_snake("userId"), "user_id");
322    }
323
324    #[test]
325    fn test_snake_to_camel() {
326        assert_eq!(snake_to_camel("user_name"), "userName");
327        assert_eq!(snake_to_camel("user_id"), "userId");
328    }
329
330    #[test]
331    fn test_md5() {
332        assert_eq!(md5("hello"), "5d41402abc4b2a76b9719d911017c592");
333    }
334
335    #[test]
336    fn test_uuid() {
337        let id = uuid();
338        assert_eq!(id.len(), 36);
339        assert!(id.contains('-'));
340    }
341}