Skip to main content

alun_utils/
valid.rs

1//! 常规验证工具:邮箱、手机、URL、UUID、身份证等
2
3use regex::Regex;
4use std::sync::OnceLock;
5
6/// 通用验证工具
7///
8/// 提供邮箱、手机号、URL、数字、字母数字、长度范围、
9/// 密码强度、IPv4、Base64、UUID、身份证、日期、JSON 等常用格式的布尔校验方法。
10pub struct Valid;
11
12impl Valid {
13    // ---- 正则缓存 ----
14
15    fn email_re() -> &'static Regex {
16        static RE: OnceLock<Regex> = OnceLock::new();
17        RE.get_or_init(|| Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap())
18    }
19
20    fn mobile_re() -> &'static Regex {
21        static RE: OnceLock<Regex> = OnceLock::new();
22        RE.get_or_init(|| Regex::new(r"^(1[3-9]\d{9})|(0\d{2,3}-\d{7,8})$").unwrap())
23    }
24
25    fn phone_re() -> &'static Regex {
26        static RE: OnceLock<Regex> = OnceLock::new();
27        RE.get_or_init(|| Regex::new(r"^\+?[1-9]\d{1,14}$").unwrap())
28    }
29
30    fn url_re() -> &'static Regex {
31        static RE: OnceLock<Regex> = OnceLock::new();
32        RE.get_or_init(|| Regex::new(r"^https?://(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*)$").unwrap())
33    }
34
35    fn username_re() -> &'static Regex {
36        static RE: OnceLock<Regex> = OnceLock::new();
37        RE.get_or_init(|| Regex::new(r"^[a-zA-Z0-9_.-]{3,50}$").unwrap())
38    }
39
40    fn color_re() -> &'static Regex {
41        static RE: OnceLock<Regex> = OnceLock::new();
42        RE.get_or_init(|| Regex::new(r"^#[0-9A-Fa-f]{6}$").unwrap())
43    }
44
45    fn id_card_re() -> &'static Regex {
46        static RE: OnceLock<Regex> = OnceLock::new();
47        RE.get_or_init(|| Regex::new(r"^\d{17}[\dXx]$").unwrap())
48    }
49
50    fn html_re() -> &'static Regex {
51        static RE: OnceLock<Regex> = OnceLock::new();
52        RE.get_or_init(|| Regex::new(r"<[^>]*>").unwrap())
53    }
54
55    // ---- 基础验证 ----
56
57    /// 验证邮箱
58    pub fn is_email(s: &str) -> bool { Self::email_re().is_match(s) }
59
60    /// 验证手机号(中国大陆)或固话(如 010-12345678)
61    pub fn is_mobile(s: &str) -> bool { Self::mobile_re().is_match(s) }
62
63    /// 验证电话号码(E.164 格式)
64    pub fn is_phone(s: &str) -> bool { Self::phone_re().is_match(s) }
65
66    /// 验证 URL
67    pub fn is_url(s: &str) -> bool { Self::url_re().is_match(s) }
68
69    /// 验证用户名(3~50 位字母、数字、下划线、点、横线)
70    pub fn is_username(s: &str) -> bool { Self::username_re().is_match(s) }
71
72    /// 验证十六进制颜色(#RRGGBB)
73    pub fn is_color(s: &str) -> bool { Self::color_re().is_match(s) }
74
75    /// 验证是否为纯数字
76    pub fn is_digits(s: &str) -> bool { !s.is_empty() && s.chars().all(|c| c.is_ascii_digit()) }
77
78    /// 验证是否为字母+数字组合
79    pub fn is_alphanumeric(s: &str) -> bool { !s.is_empty() && s.chars().all(|c| c.is_ascii_alphanumeric()) }
80
81    /// 验证字符串长度范围
82    pub fn len_between(s: &str, min: usize, max: usize) -> bool {
83        let len = s.chars().count();
84        len >= min && len <= max
85    }
86
87    // ---- 密码 ----
88
89    /// 验证密码强度(至少 8 位,包含大小写+数字+特殊字符)
90    pub fn is_strong_password(s: &str) -> bool {
91        if s.len() < 8 { return false; }
92        let has_lower = s.chars().any(|c| c.is_ascii_lowercase());
93        let has_upper = s.chars().any(|c| c.is_ascii_uppercase());
94        let has_digit = s.chars().any(|c| c.is_ascii_digit());
95        let has_special = s.chars().any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c));
96        has_lower && has_upper && has_digit && has_special
97    }
98
99    // ---- 网络与编码 ----
100
101    /// 验证 IPv4
102    pub fn is_ipv4(s: &str) -> bool {
103        s.parse::<std::net::Ipv4Addr>().is_ok()
104    }
105
106    /// 验证 Base64
107    pub fn is_base64(s: &str) -> bool {
108        base64::Engine::decode(&base64::engine::general_purpose::STANDARD, s).is_ok() && s.len() % 4 == 0
109    }
110
111    // ---- UUID ----
112
113    /// 验证 UUID(v1~v7 均支持)
114    pub fn is_uuid(s: &str) -> bool {
115        uuid::Uuid::parse_str(s).is_ok()
116    }
117
118    // ---- 身份证 ----
119
120    /// 验证中国居民身份证号(含校验位)
121    pub fn is_id_card(s: &str) -> bool {
122        if !Self::id_card_re().is_match(s) {
123            return false;
124        }
125        Self::validate_id_card_check_digit(s)
126    }
127
128    /// 身份证校验位验证
129    fn validate_id_card_check_digit(id_card: &str) -> bool {
130        if id_card.len() != 18 {
131            return false;
132        }
133        let weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
134        let check_digits = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
135        let mut sum = 0u32;
136        for (i, ch) in id_card.chars().take(17).enumerate() {
137            match ch.to_digit(10) {
138                Some(digit) => sum += digit * weights[i],
139                None => return false,
140            }
141        }
142        let expected = check_digits[(sum % 11) as usize];
143        let actual = id_card.chars().last().unwrap_or(' ');
144        expected == actual.to_ascii_uppercase()
145    }
146
147    // ---- 日期 ----
148
149    /// 验证日期格式(YYYY-MM-DD)
150    pub fn is_date(s: &str) -> bool {
151        chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").is_ok()
152    }
153
154    /// 验证日期时间格式(ISO 8601 / RFC 3339 或 YYYY-MM-DD)
155    pub fn is_datetime(s: &str) -> bool {
156        chrono::DateTime::parse_from_rfc3339(s).is_ok()
157            || chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").is_ok()
158    }
159
160    // ---- JSON ----
161
162    /// 验证是否为合法 JSON 字符串
163    pub fn is_json(s: &str) -> bool {
164        serde_json::from_str::<serde_json::Value>(s).is_ok()
165    }
166
167    // ---- HTML ----
168
169    /// 检测字符串是否包含 HTML 标签
170    pub fn has_html(s: &str) -> bool {
171        Self::html_re().is_match(s)
172    }
173
174    /// 检测字符串是否不包含 HTML 标签
175    pub fn is_html_free(s: &str) -> bool {
176        !Self::has_html(s)
177    }
178
179    // ---- 文件 ----
180
181    /// 验证文件扩展名是否在允许列表中
182    pub fn is_file_extension(filename: &str, allowed: &[&str]) -> bool {
183        if let Some(ext) = std::path::Path::new(filename)
184            .extension()
185            .and_then(|e| e.to_str())
186        {
187            let ext_lower = ext.to_lowercase();
188            return allowed.iter().any(|a| a.to_lowercase() == ext_lower);
189        }
190        false
191    }
192}
193
194/// 将 validator crate 的 ValidationErrors 转换为可读的错误消息
195///
196/// 需要启用 `validator-integration` feature
197#[cfg(feature = "validator")]
198impl Valid {
199    pub fn format_validation_errors(errors: &validator::ValidationErrors) -> String {
200        let mut messages = Vec::new();
201        for (field, field_errors) in errors.field_errors() {
202            for error in field_errors {
203                if let Some(msg) = &error.message {
204                    messages.push(format!("{}: {}", field, msg));
205                } else {
206                    messages.push(format!("{}: {}", field, error.code));
207                }
208            }
209        }
210        messages.join("; ")
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn test_email() {
220        assert!(Valid::is_email("a@b.com"));
221        assert!(!Valid::is_email("not-email"));
222    }
223
224    #[test]
225    fn test_mobile() {
226        assert!(Valid::is_mobile("13812345678"));
227        assert!(Valid::is_mobile("010-12345678"));
228        assert!(Valid::is_mobile("021-87654321"));
229        assert!(Valid::is_mobile("0755-12345678"));
230        assert!(!Valid::is_mobile("1234"));
231    }
232
233    #[test]
234    fn test_phone() {
235        assert!(Valid::is_phone("+8613812345678"));
236        assert!(!Valid::is_phone("not-phone"));
237    }
238
239    #[test]
240    fn test_username() {
241        assert!(Valid::is_username("john_doe"));
242        assert!(Valid::is_username("user.name-123"));
243        assert!(!Valid::is_username("ab")); // 太短
244    }
245
246    #[test]
247    fn test_color() {
248        assert!(Valid::is_color("#FF00AA"));
249        assert!(Valid::is_color("#000000"));
250        assert!(!Valid::is_color("FF00AA")); // 缺少 #
251        assert!(!Valid::is_color("#FF00A")); // 位数不足
252    }
253
254    #[test]
255    fn test_uuid() {
256        assert!(Valid::is_uuid("550e8400-e29b-41d4-a716-446655440000"));
257        assert!(Valid::is_uuid("00000000-0000-0000-0000-000000000000"));
258        assert!(!Valid::is_uuid("not-a-uuid"));
259    }
260
261    #[test]
262    fn test_id_card() {
263        assert!(!Valid::is_id_card("1234")); // 太短
264        assert!(!Valid::is_id_card("110101199003076790")); // 校验位错
265    }
266
267    #[test]
268    fn test_date() {
269        assert!(Valid::is_date("2024-01-01"));
270        assert!(!Valid::is_date("2024-13-01")); // 月份错误
271        assert!(!Valid::is_date("not-a-date"));
272    }
273
274    #[test]
275    fn test_datetime() {
276        assert!(Valid::is_datetime("2024-01-01T00:00:00Z"));
277        assert!(Valid::is_datetime("2024-01-01T00:00:00+08:00"));
278        assert!(!Valid::is_datetime("not-a-datetime"));
279    }
280
281    #[test]
282    fn test_json() {
283        assert!(Valid::is_json(r#"{"key": "value"}"#));
284        assert!(Valid::is_json(r#"[1, 2, 3]"#));
285        assert!(!Valid::is_json("not-json"));
286    }
287
288    #[test]
289    fn test_html() {
290        assert!(Valid::has_html("<div>hello</div>"));
291        assert!(Valid::has_html("<script>alert(1)</script>"));
292        assert!(!Valid::has_html("plain text"));
293        assert!(Valid::is_html_free("plain text"));
294    }
295
296    #[test]
297    fn test_file_extension() {
298        let allowed = &["jpg", "png", "gif"];
299        assert!(Valid::is_file_extension("photo.jpg", allowed));
300        assert!(Valid::is_file_extension("photo.JPG", allowed));
301        assert!(!Valid::is_file_extension("photo.pdf", allowed));
302    }
303
304    #[test]
305    fn test_password() {
306        assert!(Valid::is_strong_password("Abcdefg1!"));
307        assert!(!Valid::is_strong_password("123456")); // 太短
308        assert!(!Valid::is_strong_password("Abcdefg1")); // 缺少特殊字符
309    }
310
311    #[test]
312    fn test_url() {
313        assert!(Valid::is_url("https://example.com/path"));
314        assert!(!Valid::is_url("not-a-url"));
315    }
316
317    #[test]
318    fn test_ipv4() {
319        assert!(Valid::is_ipv4("127.0.0.1"));
320        assert!(!Valid::is_ipv4("not-ip"));
321    }
322
323    #[test]
324    fn test_base64() {
325        assert!(Valid::is_base64("SGVsbG8="));
326        assert!(!Valid::is_base64("not-base64!"));
327    }
328}