Skip to main content

alun_utils/
valid.rs

1//! 常规验证工具:邮箱、手机、URL、身份证等
2
3use regex::Regex;
4use std::sync::OnceLock;
5
6/// 通用验证工具
7///
8/// 提供邮箱、手机号、URL、数字、字母数字、长度范围、
9/// 密码强度、IPv4、Base64 等常用格式的布尔校验方法。
10pub struct Valid;
11
12impl Valid {
13    fn email_re() -> &'static Regex {
14        static RE: OnceLock<Regex> = OnceLock::new();
15        RE.get_or_init(|| Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap())
16    }
17
18    fn mobile_re() -> &'static Regex {
19        static RE: OnceLock<Regex> = OnceLock::new();
20        RE.get_or_init(|| Regex::new(r"^1[3-9]\d{9}$").unwrap())
21    }
22
23    fn url_re() -> &'static Regex {
24        static RE: OnceLock<Regex> = OnceLock::new();
25        RE.get_or_init(|| Regex::new(r"^https?://[^\s/$.?#].[^\s]*$").unwrap())
26    }
27
28    /// 验证邮箱
29    pub fn is_email(s: &str) -> bool { Self::email_re().is_match(s) }
30
31    /// 验证手机号(中国大陆)
32    pub fn is_mobile(s: &str) -> bool { Self::mobile_re().is_match(s) }
33
34    /// 验证 URL
35    pub fn is_url(s: &str) -> bool { Self::url_re().is_match(s) }
36
37    /// 验证是否为纯数字
38    pub fn is_digits(s: &str) -> bool { !s.is_empty() && s.chars().all(|c| c.is_ascii_digit()) }
39
40    /// 验证是否为字母+数字组合
41    pub fn is_alphanumeric(s: &str) -> bool { !s.is_empty() && s.chars().all(|c| c.is_ascii_alphanumeric()) }
42
43    /// 验证字符串长度范围
44    pub fn len_between(s: &str, min: usize, max: usize) -> bool {
45        let len = s.chars().count();
46        len >= min && len <= max
47    }
48
49    /// 验证密码强度(至少 8 位,包含大小写+数字)
50    pub fn is_strong_password(s: &str) -> bool {
51        if s.len() < 8 { return false; }
52        let has_lower = s.chars().any(|c| c.is_ascii_lowercase());
53        let has_upper = s.chars().any(|c| c.is_ascii_uppercase());
54        let has_digit = s.chars().any(|c| c.is_ascii_digit());
55        has_lower && has_upper && has_digit
56    }
57
58    /// 验证 IPv4
59    pub fn is_ipv4(s: &str) -> bool {
60        s.parse::<std::net::Ipv4Addr>().is_ok()
61    }
62
63    /// 验证 Base64
64    pub fn is_base64(s: &str) -> bool {
65        base64::Engine::decode(&base64::engine::general_purpose::STANDARD, s).is_ok() && s.len() % 4 == 0
66    }
67}
68
69/// 将 validator crate 的 ValidationErrors 转换为可读的错误消息
70///
71/// 需要启用 `validator-integration` feature
72#[cfg(feature = "validator")]
73impl Valid {
74    pub fn format_validation_errors(errors: &validator::ValidationErrors) -> String {
75        let mut messages = Vec::new();
76        for (field, field_errors) in errors.field_errors() {
77            for error in field_errors {
78                if let Some(msg) = &error.message {
79                    messages.push(format!("{}: {}", field, msg));
80                } else {
81                    messages.push(format!("{}: {}", field, error.code));
82                }
83            }
84        }
85        messages.join("; ")
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    #[test]
93    fn test_email() { assert!(Valid::is_email("a@b.com")); assert!(!Valid::is_email("not-email")); }
94    #[test]
95    fn test_mobile() { assert!(Valid::is_mobile("13812345678")); assert!(!Valid::is_mobile("1234")); }
96    #[test]
97    fn test_password() { assert!(Valid::is_strong_password("Abcdefg1")); assert!(!Valid::is_strong_password("123456")); }
98}