1use regex::Regex;
4use std::sync::OnceLock;
5
6pub struct Valid;
11
12impl Valid {
13 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 pub fn is_email(s: &str) -> bool { Self::email_re().is_match(s) }
59
60 pub fn is_mobile(s: &str) -> bool { Self::mobile_re().is_match(s) }
62
63 pub fn is_phone(s: &str) -> bool { Self::phone_re().is_match(s) }
65
66 pub fn is_url(s: &str) -> bool { Self::url_re().is_match(s) }
68
69 pub fn is_username(s: &str) -> bool { Self::username_re().is_match(s) }
71
72 pub fn is_color(s: &str) -> bool { Self::color_re().is_match(s) }
74
75 pub fn is_digits(s: &str) -> bool { !s.is_empty() && s.chars().all(|c| c.is_ascii_digit()) }
77
78 pub fn is_alphanumeric(s: &str) -> bool { !s.is_empty() && s.chars().all(|c| c.is_ascii_alphanumeric()) }
80
81 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 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 pub fn is_ipv4(s: &str) -> bool {
103 s.parse::<std::net::Ipv4Addr>().is_ok()
104 }
105
106 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 pub fn is_uuid(s: &str) -> bool {
115 uuid::Uuid::parse_str(s).is_ok()
116 }
117
118 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 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 pub fn is_date(s: &str) -> bool {
151 chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").is_ok()
152 }
153
154 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 pub fn is_json(s: &str) -> bool {
164 serde_json::from_str::<serde_json::Value>(s).is_ok()
165 }
166
167 pub fn has_html(s: &str) -> bool {
171 Self::html_re().is_match(s)
172 }
173
174 pub fn is_html_free(s: &str) -> bool {
176 !Self::has_html(s)
177 }
178
179 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#[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")); }
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")); assert!(!Valid::is_color("#FF00A")); }
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")); assert!(!Valid::is_id_card("110101199003076790")); }
266
267 #[test]
268 fn test_date() {
269 assert!(Valid::is_date("2024-01-01"));
270 assert!(!Valid::is_date("2024-13-01")); 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")); assert!(!Valid::is_strong_password("Abcdefg1")); }
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}