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 }
158
159 pub fn is_json(s: &str) -> bool {
163 serde_json::from_str::<serde_json::Value>(s).is_ok()
164 }
165
166 pub fn has_html(s: &str) -> bool {
170 Self::html_re().is_match(s)
171 }
172
173 pub fn is_html_free(s: &str) -> bool {
175 !Self::has_html(s)
176 }
177
178 pub fn is_file_extension(filename: &str, allowed: &[&str]) -> bool {
182 if let Some(ext) = std::path::Path::new(filename)
183 .extension()
184 .and_then(|e| e.to_str())
185 {
186 let ext_lower = ext.to_lowercase();
187 return allowed.iter().any(|a| a.to_lowercase() == ext_lower);
188 }
189 false
190 }
191}
192
193#[cfg(feature = "validator")]
197impl Valid {
198 pub fn format_validation_errors(errors: &validator::ValidationErrors) -> String {
199 let mut messages = Vec::new();
200 for (field, field_errors) in errors.field_errors() {
201 for error in field_errors {
202 if let Some(msg) = &error.message {
203 messages.push(format!("{}: {}", field, msg));
204 } else {
205 messages.push(format!("{}: {}", field, error.code));
206 }
207 }
208 }
209 messages.join("; ")
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn test_email() {
219 assert!(Valid::is_email("a@b.com"));
220 assert!(!Valid::is_email("not-email"));
221 }
222
223 #[test]
224 fn test_mobile() {
225 assert!(Valid::is_mobile("13812345678"));
226 assert!(Valid::is_mobile("010-12345678"));
227 assert!(Valid::is_mobile("021-87654321"));
228 assert!(Valid::is_mobile("0755-12345678"));
229 assert!(!Valid::is_mobile("1234"));
230 }
231
232 #[test]
233 fn test_phone() {
234 assert!(Valid::is_phone("+8613812345678"));
235 assert!(!Valid::is_phone("not-phone"));
236 }
237
238 #[test]
239 fn test_username() {
240 assert!(Valid::is_username("john_doe"));
241 assert!(Valid::is_username("user.name-123"));
242 assert!(!Valid::is_username("ab")); }
244
245 #[test]
246 fn test_color() {
247 assert!(Valid::is_color("#FF00AA"));
248 assert!(Valid::is_color("#000000"));
249 assert!(!Valid::is_color("FF00AA")); assert!(!Valid::is_color("#FF00A")); }
252
253 #[test]
254 fn test_uuid() {
255 assert!(Valid::is_uuid("550e8400-e29b-41d4-a716-446655440000"));
256 assert!(Valid::is_uuid("00000000-0000-0000-0000-000000000000"));
257 assert!(!Valid::is_uuid("not-a-uuid"));
258 }
259
260 #[test]
261 fn test_id_card() {
262 assert!(!Valid::is_id_card("1234")); assert!(!Valid::is_id_card("110101199003076790")); }
265
266 #[test]
267 fn test_date() {
268 assert!(Valid::is_date("2024-01-01"));
269 assert!(!Valid::is_date("2024-13-01")); assert!(!Valid::is_date("not-a-date"));
271 }
272
273 #[test]
274 fn test_datetime() {
275 assert!(Valid::is_datetime("2024-01-01T00:00:00Z"));
276 assert!(Valid::is_datetime("2024-01-01T00:00:00+08:00"));
277 assert!(!Valid::is_datetime("not-a-datetime"));
278 }
279
280 #[test]
281 fn test_json() {
282 assert!(Valid::is_json(r#"{"key": "value"}"#));
283 assert!(Valid::is_json(r#"[1, 2, 3]"#));
284 assert!(!Valid::is_json("not-json"));
285 }
286
287 #[test]
288 fn test_html() {
289 assert!(Valid::has_html("<div>hello</div>"));
290 assert!(Valid::has_html("<script>alert(1)</script>"));
291 assert!(!Valid::has_html("plain text"));
292 assert!(Valid::is_html_free("plain text"));
293 }
294
295 #[test]
296 fn test_file_extension() {
297 let allowed = &["jpg", "png", "gif"];
298 assert!(Valid::is_file_extension("photo.jpg", allowed));
299 assert!(Valid::is_file_extension("photo.JPG", allowed));
300 assert!(!Valid::is_file_extension("photo.pdf", allowed));
301 }
302
303 #[test]
304 fn test_password() {
305 assert!(Valid::is_strong_password("Abcdefg1!"));
306 assert!(!Valid::is_strong_password("123456")); assert!(!Valid::is_strong_password("Abcdefg1")); }
309
310 #[test]
311 fn test_url() {
312 assert!(Valid::is_url("https://example.com/path"));
313 assert!(!Valid::is_url("not-a-url"));
314 }
315
316 #[test]
317 fn test_ipv4() {
318 assert!(Valid::is_ipv4("127.0.0.1"));
319 assert!(!Valid::is_ipv4("not-ip"));
320 }
321
322 #[test]
323 fn test_base64() {
324 assert!(Valid::is_base64("SGVsbG8="));
325 assert!(!Valid::is_base64("not-base64!"));
326 }
327}