1use serde_json::Value;
2
3use crate::error::{value_type_name, IssueCode, StringValidation, VldError};
4use crate::schema::VldSchema;
5
6fn is_valid_email(s: &str) -> bool {
11 let at = match s.find('@') {
13 Some(pos) if pos > 0 => pos,
14 _ => return false,
15 };
16 let local = &s[..at];
17 let domain = &s[at + 1..];
18
19 if local.is_empty() || domain.is_empty() {
20 return false;
21 }
22
23 for ch in local.chars() {
25 if ch.is_ascii_alphanumeric() || "!#$%&'*+/=?^_`{|}~.-".contains(ch) {
26 continue;
27 }
28 return false;
29 }
30
31 is_valid_hostname(domain)
32}
33
34fn is_valid_uuid(s: &str) -> bool {
35 if s.len() != 36 {
37 return false;
38 }
39 let bytes = s.as_bytes();
40 for (i, &b) in bytes.iter().enumerate() {
41 match i {
42 8 | 13 | 18 | 23 => {
43 if b != b'-' {
44 return false;
45 }
46 }
47 _ => {
48 if !b.is_ascii_hexdigit() {
49 return false;
50 }
51 }
52 }
53 }
54 true
55}
56
57#[cfg(feature = "string-advanced")]
58fn is_valid_uuid_version(s: &str, version: usize) -> bool {
59 let u = match uuid::Uuid::parse_str(s) {
60 Ok(v) => v,
61 Err(_) => return false,
62 };
63 u.get_version_num() == version
64}
65
66fn is_valid_url(s: &str) -> bool {
67 let rest = if let Some(r) = s.strip_prefix("https://") {
69 r
70 } else if let Some(r) = s.strip_prefix("http://") {
71 r
72 } else {
73 return false;
74 };
75 if rest.is_empty() {
76 return false;
77 }
78 !rest.contains(char::is_whitespace)
80}
81
82#[cfg(feature = "string-advanced")]
83fn is_valid_url_strict(s: &str) -> bool {
84 let parsed = match url::Url::parse(s) {
85 Ok(v) => v,
86 Err(_) => return false,
87 };
88 if parsed.scheme() != "http" && parsed.scheme() != "https" {
89 return false;
90 }
91 let host = match parsed.host_str() {
92 Some(h) => h,
93 None => return false,
94 };
95 if host.eq_ignore_ascii_case("localhost") {
96 return false;
97 }
98 if let Ok(ip) = host.parse::<std::net::IpAddr>() {
99 match ip {
100 std::net::IpAddr::V4(v4) => {
101 if v4.is_private() || v4.is_loopback() || v4.is_link_local() {
102 return false;
103 }
104 }
105 std::net::IpAddr::V6(v6) => {
106 let seg0 = v6.segments()[0];
107 let is_link_local = (seg0 & 0xffc0) == 0xfe80; let is_unique_local = (seg0 & 0xfe00) == 0xfc00; if v6.is_loopback() || is_link_local || is_unique_local {
110 return false;
111 }
112 }
113 }
114 }
115 true
116}
117
118#[cfg(feature = "string-advanced")]
119fn is_valid_uri(s: &str) -> bool {
120 url::Url::parse(s).is_ok()
121}
122
123fn is_valid_ipv4(s: &str) -> bool {
124 let parts: Vec<&str> = s.split('.').collect();
125 if parts.len() != 4 {
126 return false;
127 }
128 for part in parts {
129 if part.is_empty() || part.len() > 3 {
130 return false;
131 }
132 if part.len() > 1 && part.starts_with('0') {
134 return false;
135 }
136 match part.parse::<u16>() {
137 Ok(n) if n <= 255 => {}
138 _ => return false,
139 }
140 }
141 true
142}
143
144fn is_valid_ipv6(s: &str) -> bool {
145 if s == "::" {
147 return true;
148 }
149
150 let (left, right) = if let Some(pos) = s.find("::") {
151 (&s[..pos], &s[pos + 2..])
152 } else {
153 (s, "")
154 };
155
156 let has_double_colon = s.contains("::");
157
158 let left_groups: Vec<&str> = if left.is_empty() {
159 vec![]
160 } else {
161 left.split(':').collect()
162 };
163
164 let right_groups: Vec<&str> = if right.is_empty() {
165 vec![]
166 } else {
167 right.split(':').collect()
168 };
169
170 let total = left_groups.len() + right_groups.len();
171
172 if has_double_colon {
173 if total > 7 {
174 return false;
175 }
176 } else if total != 8 {
177 return false;
178 }
179
180 for group in left_groups.iter().chain(right_groups.iter()) {
181 if group.is_empty() || group.len() > 4 {
182 return false;
183 }
184 if !group.chars().all(|c| c.is_ascii_hexdigit()) {
185 return false;
186 }
187 }
188
189 true
190}
191
192fn is_valid_ip(s: &str) -> bool {
193 is_valid_ipv4(s) || is_valid_ipv6(s)
194}
195
196fn is_valid_cidr(s: &str) -> bool {
197 let (ip, prefix) = match s.split_once('/') {
198 Some(v) => v,
199 None => return false,
200 };
201 let prefix = match prefix.parse::<u8>() {
202 Ok(v) => v,
203 Err(_) => return false,
204 };
205 if is_valid_ipv4(ip) {
206 prefix <= 32
207 } else if is_valid_ipv6(ip) {
208 prefix <= 128
209 } else {
210 false
211 }
212}
213
214fn is_valid_mac(s: &str) -> bool {
215 let parts: Vec<&str> = s.split(':').collect();
216 if parts.len() != 6 {
217 return false;
218 }
219 parts
220 .iter()
221 .all(|p| p.len() == 2 && p.bytes().all(|b| b.is_ascii_hexdigit()))
222}
223
224fn is_valid_hex(s: &str) -> bool {
225 !s.is_empty() && s.bytes().all(|b| b.is_ascii_hexdigit())
226}
227
228fn is_valid_credit_card(s: &str) -> bool {
229 let digits: Vec<u32> = s
230 .chars()
231 .filter(|c| c.is_ascii_digit())
232 .filter_map(|c| c.to_digit(10))
233 .collect();
234 if digits.len() < 12 || digits.len() > 19 {
235 return false;
236 }
237 let mut sum = 0;
238 let mut double = false;
239 for d in digits.iter().rev() {
240 let mut v = *d;
241 if double {
242 v *= 2;
243 if v > 9 {
244 v -= 9;
245 }
246 }
247 sum += v;
248 double = !double;
249 }
250 sum % 10 == 0
251}
252
253fn is_valid_phone(s: &str) -> bool {
254 if s.is_empty() {
255 return false;
256 }
257 let mut chars = s.chars();
258 let first = chars.next().unwrap_or_default();
259 if first != '+' {
260 return false;
261 }
262 let digits: String = chars.filter(|c| c.is_ascii_digit()).collect();
263 (8..=15).contains(&digits.len())
265}
266
267#[cfg(feature = "string-advanced")]
268fn is_valid_phone_e164_strict(s: &str) -> bool {
269 let num = match phonenumber::parse(None, s) {
270 Ok(n) => n,
271 Err(_) => return false,
272 };
273 phonenumber::is_valid(&num)
274}
275
276fn is_valid_slug(s: &str) -> bool {
277 if s.is_empty() || s.starts_with('-') || s.ends_with('-') {
278 return false;
279 }
280 s.bytes()
281 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
282}
283
284fn is_valid_color(s: &str) -> bool {
285 if let Some(hex) = s.strip_prefix('#') {
286 return (hex.len() == 6 || hex.len() == 8) && hex.bytes().all(|b| b.is_ascii_hexdigit());
287 }
288 let lower = s.to_ascii_lowercase();
289 if lower.starts_with("rgb(") && lower.ends_with(')') {
290 let inner = &lower[4..lower.len() - 1];
291 let parts: Vec<&str> = inner.split(',').map(|p| p.trim()).collect();
292 if parts.len() != 3 {
293 return false;
294 }
295 return parts.iter().all(|p| p.parse::<u8>().is_ok());
296 }
297 if lower.starts_with("hsl(") && lower.ends_with(')') {
298 let inner = &lower[4..lower.len() - 1];
299 let parts: Vec<&str> = inner.split(',').map(|p| p.trim()).collect();
300 if parts.len() != 3 {
301 return false;
302 }
303 let h_ok = parts[0].parse::<u16>().map(|h| h <= 360).unwrap_or(false);
304 let s_ok = parts[1]
305 .strip_suffix('%')
306 .and_then(|v| v.parse::<u8>().ok())
307 .map(|v| v <= 100)
308 .unwrap_or(false);
309 let l_ok = parts[2]
310 .strip_suffix('%')
311 .and_then(|v| v.parse::<u8>().ok())
312 .map(|v| v <= 100)
313 .unwrap_or(false);
314 return h_ok && s_ok && l_ok;
315 }
316 false
317}
318
319fn is_valid_currency_code(s: &str) -> bool {
320 s.len() == 3 && s.bytes().all(|b| b.is_ascii_uppercase())
321}
322
323fn is_valid_country_code(s: &str) -> bool {
324 s.len() == 2 && s.bytes().all(|b| b.is_ascii_uppercase())
325}
326
327fn is_valid_locale(s: &str) -> bool {
328 let bytes = s.as_bytes();
329 if bytes.len() == 2 {
330 return bytes[0].is_ascii_lowercase() && bytes[1].is_ascii_lowercase();
331 }
332 if bytes.len() == 5 && bytes[2] == b'-' {
333 return bytes[0].is_ascii_lowercase()
334 && bytes[1].is_ascii_lowercase()
335 && bytes[3].is_ascii_uppercase()
336 && bytes[4].is_ascii_uppercase();
337 }
338 false
339}
340
341fn is_valid_cron(s: &str) -> bool {
342 let fields: Vec<&str> = s.split_whitespace().collect();
343 if fields.len() != 5 && fields.len() != 6 {
344 return false;
345 }
346 fields.iter().all(|f| {
347 !f.is_empty()
348 && f.bytes().all(|b| {
349 b.is_ascii_digit() || b == b'*' || b == b'/' || b == b',' || b == b'-' || b == b'?'
350 })
351 })
352}
353
354fn is_valid_semver(s: &str) -> bool {
355 let s = s.trim();
356 if s.is_empty() {
357 return false;
358 }
359 let s = s.strip_prefix('v').unwrap_or(s);
360 let main = s.split(['-', '+']).next().unwrap_or("");
361 let parts: Vec<&str> = main.split('.').collect();
362 if parts.len() != 3 {
363 return false;
364 }
365 parts
366 .iter()
367 .all(|p| !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit()))
368}
369
370#[cfg(feature = "string-advanced")]
371fn is_valid_semver_full(s: &str) -> bool {
372 semver::Version::parse(s).is_ok()
373}
374
375fn is_valid_base64_url(s: &str) -> bool {
376 !s.is_empty()
377 && s.bytes()
378 .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
379}
380
381fn is_valid_jwt(s: &str) -> bool {
382 let parts: Vec<&str> = s.split('.').collect();
383 if parts.len() != 3 {
384 return false;
385 }
386 parts.iter().all(|p| is_valid_base64_url(p))
387}
388
389fn is_valid_base64(s: &str) -> bool {
390 if s.is_empty() || s.len() % 4 != 0 {
391 return false;
392 }
393 let mut pad_started = false;
394 for &b in s.as_bytes() {
395 if pad_started {
396 if b != b'=' {
397 return false;
398 }
399 } else if b == b'=' {
400 pad_started = true;
401 } else if !(b.is_ascii_alphanumeric() || b == b'+' || b == b'/') {
402 return false;
403 }
404 }
405 let pad_count = s.bytes().rev().take_while(|&b| b == b'=').count();
407 pad_count <= 2
408}
409
410fn parse_digits(s: &str, n: usize) -> Option<(u32, &str)> {
412 if s.len() < n {
413 return None;
414 }
415 let (digits, rest) = s.split_at(n);
416 if !digits.bytes().all(|b| b.is_ascii_digit()) {
417 return None;
418 }
419 digits.parse::<u32>().ok().map(|v| (v, rest))
420}
421
422fn is_valid_iso_date(s: &str) -> bool {
423 if s.len() != 10 {
425 return false;
426 }
427 let (year, rest) = match parse_digits(s, 4) {
428 Some(v) => v,
429 None => return false,
430 };
431 let rest = rest.strip_prefix('-').unwrap_or("");
432 let (month, rest) = match parse_digits(rest, 2) {
433 Some(v) => v,
434 None => return false,
435 };
436 let rest = rest.strip_prefix('-').unwrap_or("");
437 let (day, rest) = match parse_digits(rest, 2) {
438 Some(v) => v,
439 None => return false,
440 };
441 if !rest.is_empty() {
442 return false;
443 }
444 let _ = year; (1..=12).contains(&month) && (1..=31).contains(&day)
446}
447
448fn is_valid_iso_time(s: &str) -> bool {
449 if s.len() < 5 {
451 return false;
452 }
453 let (hour, rest) = match parse_digits(s, 2) {
454 Some(v) => v,
455 None => return false,
456 };
457 let rest = match rest.strip_prefix(':') {
458 Some(r) => r,
459 None => return false,
460 };
461 let (min, rest) = match parse_digits(rest, 2) {
462 Some(v) => v,
463 None => return false,
464 };
465 if !(0..=23).contains(&hour) || !(0..=59).contains(&min) {
466 return false;
467 }
468 if rest.is_empty() {
469 return true;
470 }
471 let rest = match rest.strip_prefix(':') {
473 Some(r) => r,
474 None => return false,
475 };
476 let (sec, rest) = match parse_digits(rest, 2) {
477 Some(v) => v,
478 None => return false,
479 };
480 if !(0..=59).contains(&sec) {
481 return false;
482 }
483 if rest.is_empty() {
484 return true;
485 }
486 let rest = match rest.strip_prefix('.') {
488 Some(r) => r,
489 None => return false,
490 };
491 !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit())
492}
493
494fn is_valid_iso_datetime(s: &str) -> bool {
495 let t_pos = match s.find('T') {
497 Some(p) => p,
498 None => return false,
499 };
500 let date_part = &s[..t_pos];
501 let after_t = &s[t_pos + 1..];
502
503 if !is_valid_iso_date(date_part) {
504 return false;
505 }
506
507 let (time_part, _tz_part) = if let Some(pos) = after_t.rfind('Z') {
509 (&after_t[..pos], &after_t[pos..])
510 } else if let Some(pos) = after_t.rfind('+') {
511 (&after_t[..pos], &after_t[pos..])
512 } else if let Some(pos) = after_t[1..].rfind('-') {
513 let actual = pos + 1;
515 (&after_t[..actual], &after_t[actual..])
516 } else {
517 (after_t, "")
518 };
519
520 is_valid_iso_time(time_part)
521}
522
523fn is_valid_hostname(s: &str) -> bool {
524 if s.is_empty() || s.len() > 253 {
525 return false;
526 }
527 for label in s.split('.') {
528 if label.is_empty() || label.len() > 63 {
529 return false;
530 }
531 if label.starts_with('-') || label.ends_with('-') {
532 return false;
533 }
534 if !label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
535 return false;
536 }
537 }
538 true
539}
540
541fn is_valid_cuid2(s: &str) -> bool {
542 if s.is_empty() {
543 return false;
544 }
545 if !s.as_bytes()[0].is_ascii_lowercase() {
546 return false;
547 }
548 s.chars()
549 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
550}
551
552fn is_valid_ulid(s: &str) -> bool {
553 if s.len() != 26 {
554 return false;
555 }
556 s.chars().all(|c| {
558 let c = c.to_ascii_uppercase();
559 matches!(c, '0'..='9' | 'A'..='H' | 'J' | 'K' | 'M' | 'N' | 'P'..='T' | 'V'..='Z')
560 })
561}
562
563fn is_valid_nanoid(s: &str) -> bool {
564 if s.is_empty() {
565 return false;
566 }
567 s.chars()
568 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
569}
570
571fn is_valid_emoji(s: &str) -> bool {
572 s.chars().any(|c| {
573 let cp = c as u32;
574 (0x1F600..=0x1F64F).contains(&cp)
575 || (0x1F300..=0x1F5FF).contains(&cp)
576 || (0x1F680..=0x1F6FF).contains(&cp)
577 || (0x1F1E0..=0x1F1FF).contains(&cp)
578 || (0x2702..=0x27B0).contains(&cp)
579 || (0x2600..=0x26FF).contains(&cp)
580 || (0xFE00..=0xFE0F).contains(&cp)
581 || (0x1F900..=0x1F9FF).contains(&cp)
582 || (0x1FA00..=0x1FA6F).contains(&cp)
583 || (0x1FA70..=0x1FAFF).contains(&cp)
584 || (0x231A..=0x231B).contains(&cp)
585 || (0x23E9..=0x23F3).contains(&cp)
586 || (0x23F8..=0x23FA).contains(&cp)
587 || cp == 0x200D
588 || cp == 0x2B50
589 || cp == 0x2764
590 })
591}
592
593#[derive(Clone)]
598enum StringCheck {
599 Min(usize, String),
600 Max(usize, String),
601 Len(usize, String),
602 Email(String),
603 Url(String),
604 #[cfg(feature = "string-advanced")]
605 UrlStrict(String),
606 #[cfg(feature = "string-advanced")]
607 Uri(String),
608 Uuid(String),
609 #[cfg(feature = "string-advanced")]
610 UuidV1(String),
611 #[cfg(feature = "string-advanced")]
612 UuidV4(String),
613 #[cfg(feature = "string-advanced")]
614 UuidV7(String),
615 Ip(String),
616 Slug(String),
617 Color(String),
618 Currency(String),
619 CountryCode(String),
620 Locale(String),
621 Cron(String),
622 #[cfg(feature = "regex")]
623 Regex(regex_lite::Regex, String),
624 StartsWith(String, String),
625 EndsWith(String, String),
626 Contains(String, String),
627 NonEmpty(String),
628 Ipv4(String),
629 Ipv6(String),
630 Cidr(String),
631 Mac(String),
632 Hex(String),
633 CreditCard(String),
634 Phone(String),
635 #[cfg(feature = "string-advanced")]
636 PhoneE164(String),
637 Semver(String),
638 #[cfg(feature = "string-advanced")]
639 SemverFull(String),
640 Jwt(String),
641 Ascii(String),
642 Alpha(String),
643 Alphanumeric(String),
644 Lowercase(String),
645 Uppercase(String),
646 Base64(String),
647 IsoDate(String),
648 IsoDatetime(String),
649 IsoTime(String),
650 Hostname(String),
651 Cuid2(String),
652 Ulid(String),
653 Nanoid(String),
654 Emoji(String),
655}
656
657impl StringCheck {
658 fn key(&self) -> &str {
660 match self {
661 StringCheck::Min(..) => "too_small",
662 StringCheck::Max(..) => "too_big",
663 StringCheck::Len(..) => "invalid_length",
664 StringCheck::Email(..) => "invalid_email",
665 StringCheck::Url(..) => "invalid_url",
666 #[cfg(feature = "string-advanced")]
667 StringCheck::UrlStrict(..) => "invalid_url_strict",
668 #[cfg(feature = "string-advanced")]
669 StringCheck::Uri(..) => "invalid_uri",
670 StringCheck::Uuid(..) => "invalid_uuid",
671 #[cfg(feature = "string-advanced")]
672 StringCheck::UuidV1(..) => "invalid_uuid_v1",
673 #[cfg(feature = "string-advanced")]
674 StringCheck::UuidV4(..) => "invalid_uuid_v4",
675 #[cfg(feature = "string-advanced")]
676 StringCheck::UuidV7(..) => "invalid_uuid_v7",
677 StringCheck::Ip(..) => "invalid_ip",
678 StringCheck::Slug(..) => "invalid_slug",
679 StringCheck::Color(..) => "invalid_color",
680 StringCheck::Currency(..) => "invalid_currency",
681 StringCheck::CountryCode(..) => "invalid_country_code",
682 StringCheck::Locale(..) => "invalid_locale",
683 StringCheck::Cron(..) => "invalid_cron",
684 #[cfg(feature = "regex")]
685 StringCheck::Regex(..) => "invalid_regex",
686 StringCheck::StartsWith(..) => "invalid_starts_with",
687 StringCheck::EndsWith(..) => "invalid_ends_with",
688 StringCheck::Contains(..) => "invalid_contains",
689 StringCheck::NonEmpty(..) => "non_empty",
690 StringCheck::Ipv4(..) => "invalid_ipv4",
691 StringCheck::Ipv6(..) => "invalid_ipv6",
692 StringCheck::Cidr(..) => "invalid_cidr",
693 StringCheck::Mac(..) => "invalid_mac",
694 StringCheck::Hex(..) => "invalid_hex",
695 StringCheck::CreditCard(..) => "invalid_credit_card",
696 StringCheck::Phone(..) => "invalid_phone",
697 #[cfg(feature = "string-advanced")]
698 StringCheck::PhoneE164(..) => "invalid_phone_e164",
699 StringCheck::Semver(..) => "invalid_semver",
700 #[cfg(feature = "string-advanced")]
701 StringCheck::SemverFull(..) => "invalid_semver_full",
702 StringCheck::Jwt(..) => "invalid_jwt",
703 StringCheck::Ascii(..) => "invalid_ascii",
704 StringCheck::Alpha(..) => "invalid_alpha",
705 StringCheck::Alphanumeric(..) => "invalid_alphanumeric",
706 StringCheck::Lowercase(..) => "invalid_lowercase",
707 StringCheck::Uppercase(..) => "invalid_uppercase",
708 StringCheck::Base64(..) => "invalid_base64",
709 StringCheck::IsoDate(..) => "invalid_iso_date",
710 StringCheck::IsoDatetime(..) => "invalid_iso_datetime",
711 StringCheck::IsoTime(..) => "invalid_iso_time",
712 StringCheck::Hostname(..) => "invalid_hostname",
713 StringCheck::Cuid2(..) => "invalid_cuid2",
714 StringCheck::Ulid(..) => "invalid_ulid",
715 StringCheck::Nanoid(..) => "invalid_nanoid",
716 StringCheck::Emoji(..) => "invalid_emoji",
717 }
718 }
719
720 fn set_message(&mut self, msg: String) {
722 match self {
723 #[cfg(feature = "string-advanced")]
724 StringCheck::UrlStrict(ref mut m)
725 | StringCheck::Uri(ref mut m)
726 | StringCheck::UuidV1(ref mut m)
727 | StringCheck::UuidV4(ref mut m)
728 | StringCheck::UuidV7(ref mut m)
729 | StringCheck::PhoneE164(ref mut m)
730 | StringCheck::SemverFull(ref mut m) => *m = msg,
731 StringCheck::Min(_, ref mut m)
732 | StringCheck::Max(_, ref mut m)
733 | StringCheck::Len(_, ref mut m)
734 | StringCheck::Email(ref mut m)
735 | StringCheck::Url(ref mut m)
736 | StringCheck::Uuid(ref mut m)
737 | StringCheck::Ip(ref mut m)
738 | StringCheck::Slug(ref mut m)
739 | StringCheck::Color(ref mut m)
740 | StringCheck::Currency(ref mut m)
741 | StringCheck::CountryCode(ref mut m)
742 | StringCheck::Locale(ref mut m)
743 | StringCheck::Cron(ref mut m)
744 | StringCheck::NonEmpty(ref mut m)
745 | StringCheck::Ipv4(ref mut m)
746 | StringCheck::Ipv6(ref mut m)
747 | StringCheck::Cidr(ref mut m)
748 | StringCheck::Mac(ref mut m)
749 | StringCheck::Hex(ref mut m)
750 | StringCheck::CreditCard(ref mut m)
751 | StringCheck::Phone(ref mut m)
752 | StringCheck::Semver(ref mut m)
753 | StringCheck::Jwt(ref mut m)
754 | StringCheck::Ascii(ref mut m)
755 | StringCheck::Alpha(ref mut m)
756 | StringCheck::Alphanumeric(ref mut m)
757 | StringCheck::Lowercase(ref mut m)
758 | StringCheck::Uppercase(ref mut m)
759 | StringCheck::Base64(ref mut m)
760 | StringCheck::IsoDate(ref mut m)
761 | StringCheck::IsoDatetime(ref mut m)
762 | StringCheck::IsoTime(ref mut m)
763 | StringCheck::Hostname(ref mut m)
764 | StringCheck::Cuid2(ref mut m)
765 | StringCheck::Ulid(ref mut m)
766 | StringCheck::Nanoid(ref mut m)
767 | StringCheck::Emoji(ref mut m) => *m = msg,
768 StringCheck::StartsWith(_, ref mut m)
769 | StringCheck::EndsWith(_, ref mut m)
770 | StringCheck::Contains(_, ref mut m) => *m = msg,
771 #[cfg(feature = "regex")]
772 StringCheck::Regex(_, ref mut m) => *m = msg,
773 }
774 }
775}
776
777#[derive(Clone)]
778enum StringTransform {
779 Trim,
780 ToLowerCase,
781 ToUpperCase,
782}
783
784#[derive(Clone)]
793pub struct ZString {
794 checks: Vec<StringCheck>,
795 transforms: Vec<StringTransform>,
796 coerce: bool,
797 custom_type_error: Option<String>,
798}
799
800impl ZString {
801 pub fn new() -> Self {
802 Self {
803 checks: vec![],
804 transforms: vec![],
805 coerce: false,
806 custom_type_error: None,
807 }
808 }
809
810 pub fn type_error(mut self, msg: impl Into<String>) -> Self {
820 self.custom_type_error = Some(msg.into());
821 self
822 }
823
824 pub fn with_messages<F>(mut self, f: F) -> Self
850 where
851 F: Fn(&str) -> Option<String>,
852 {
853 for check in &mut self.checks {
854 if let Some(msg) = f(check.key()) {
855 check.set_message(msg);
856 }
857 }
858 self
859 }
860
861 pub fn min(self, len: usize) -> Self {
863 self.min_msg(len, format!("String must be at least {} characters", len))
864 }
865
866 pub fn min_msg(mut self, len: usize, msg: impl Into<String>) -> Self {
868 self.checks.push(StringCheck::Min(len, msg.into()));
869 self
870 }
871
872 pub fn max(self, len: usize) -> Self {
874 self.max_msg(len, format!("String must be at most {} characters", len))
875 }
876
877 pub fn max_msg(mut self, len: usize, msg: impl Into<String>) -> Self {
879 self.checks.push(StringCheck::Max(len, msg.into()));
880 self
881 }
882
883 pub fn len(self, len: usize) -> Self {
885 self.len_msg(len, format!("String must be exactly {} characters", len))
886 }
887
888 pub fn len_msg(mut self, len: usize, msg: impl Into<String>) -> Self {
890 self.checks.push(StringCheck::Len(len, msg.into()));
891 self
892 }
893
894 pub fn email(self) -> Self {
896 self.email_msg("Invalid email address")
897 }
898
899 pub fn email_msg(mut self, msg: impl Into<String>) -> Self {
901 self.checks.push(StringCheck::Email(msg.into()));
902 self
903 }
904
905 pub fn url(self) -> Self {
907 self.url_msg("Invalid URL")
908 }
909
910 pub fn url_msg(mut self, msg: impl Into<String>) -> Self {
912 self.checks.push(StringCheck::Url(msg.into()));
913 self
914 }
915
916 #[cfg(feature = "string-advanced")]
918 pub fn url_strict(self) -> Self {
919 self.url_strict_msg("Invalid strict URL")
920 }
921
922 #[cfg(feature = "string-advanced")]
924 pub fn url_strict_msg(mut self, msg: impl Into<String>) -> Self {
925 self.checks.push(StringCheck::UrlStrict(msg.into()));
926 self
927 }
928
929 #[cfg(feature = "string-advanced")]
931 pub fn uri(self) -> Self {
932 self.uri_msg("Invalid URI")
933 }
934
935 #[cfg(feature = "string-advanced")]
937 pub fn uri_msg(mut self, msg: impl Into<String>) -> Self {
938 self.checks.push(StringCheck::Uri(msg.into()));
939 self
940 }
941
942 pub fn uuid(self) -> Self {
944 self.uuid_msg("Invalid UUID")
945 }
946
947 pub fn uuid_msg(mut self, msg: impl Into<String>) -> Self {
949 self.checks.push(StringCheck::Uuid(msg.into()));
950 self
951 }
952
953 #[cfg(feature = "string-advanced")]
955 pub fn uuid_v1(self) -> Self {
956 self.uuid_v1_msg("Invalid UUID v1")
957 }
958
959 #[cfg(feature = "string-advanced")]
960 pub fn uuid_v1_msg(mut self, msg: impl Into<String>) -> Self {
961 self.checks.push(StringCheck::UuidV1(msg.into()));
962 self
963 }
964
965 #[cfg(feature = "string-advanced")]
967 pub fn uuid_v4(self) -> Self {
968 self.uuid_v4_msg("Invalid UUID v4")
969 }
970
971 #[cfg(feature = "string-advanced")]
972 pub fn uuid_v4_msg(mut self, msg: impl Into<String>) -> Self {
973 self.checks.push(StringCheck::UuidV4(msg.into()));
974 self
975 }
976
977 #[cfg(feature = "string-advanced")]
979 pub fn uuid_v7(self) -> Self {
980 self.uuid_v7_msg("Invalid UUID v7")
981 }
982
983 #[cfg(feature = "string-advanced")]
984 pub fn uuid_v7_msg(mut self, msg: impl Into<String>) -> Self {
985 self.checks.push(StringCheck::UuidV7(msg.into()));
986 self
987 }
988
989 pub fn ip(self) -> Self {
991 self.ip_msg("Invalid IP address")
992 }
993
994 pub fn ip_msg(mut self, msg: impl Into<String>) -> Self {
996 self.checks.push(StringCheck::Ip(msg.into()));
997 self
998 }
999
1000 #[cfg(feature = "regex")]
1004 pub fn regex(self, re: regex_lite::Regex) -> Self {
1005 self.regex_msg(re, "String does not match pattern")
1006 }
1007
1008 #[cfg(feature = "regex")]
1012 pub fn regex_msg(mut self, re: regex_lite::Regex, msg: impl Into<String>) -> Self {
1013 self.checks.push(StringCheck::Regex(re, msg.into()));
1014 self
1015 }
1016
1017 pub fn starts_with(self, prefix: impl Into<String>) -> Self {
1019 let p = prefix.into();
1020 let msg = format!("String must start with \"{}\"", p);
1021 self.starts_with_msg(p, msg)
1022 }
1023
1024 pub fn starts_with_msg(mut self, prefix: impl Into<String>, msg: impl Into<String>) -> Self {
1026 self.checks
1027 .push(StringCheck::StartsWith(prefix.into(), msg.into()));
1028 self
1029 }
1030
1031 pub fn ends_with(self, suffix: impl Into<String>) -> Self {
1033 let s = suffix.into();
1034 let msg = format!("String must end with \"{}\"", s);
1035 self.ends_with_msg(s, msg)
1036 }
1037
1038 pub fn ends_with_msg(mut self, suffix: impl Into<String>, msg: impl Into<String>) -> Self {
1040 self.checks
1041 .push(StringCheck::EndsWith(suffix.into(), msg.into()));
1042 self
1043 }
1044
1045 pub fn contains(self, sub: impl Into<String>) -> Self {
1047 let s = sub.into();
1048 let msg = format!("String must contain \"{}\"", s);
1049 self.contains_msg(s, msg)
1050 }
1051
1052 pub fn contains_msg(mut self, sub: impl Into<String>, msg: impl Into<String>) -> Self {
1054 self.checks
1055 .push(StringCheck::Contains(sub.into(), msg.into()));
1056 self
1057 }
1058
1059 pub fn non_empty(self) -> Self {
1061 self.non_empty_msg("String must not be empty")
1062 }
1063
1064 pub fn non_empty_msg(mut self, msg: impl Into<String>) -> Self {
1066 self.checks.push(StringCheck::NonEmpty(msg.into()));
1067 self
1068 }
1069
1070 pub fn trim(mut self) -> Self {
1072 self.transforms.push(StringTransform::Trim);
1073 self
1074 }
1075
1076 pub fn to_lowercase(mut self) -> Self {
1078 self.transforms.push(StringTransform::ToLowerCase);
1079 self
1080 }
1081
1082 pub fn to_uppercase(mut self) -> Self {
1084 self.transforms.push(StringTransform::ToUpperCase);
1085 self
1086 }
1087
1088 pub fn coerce(mut self) -> Self {
1090 self.coerce = true;
1091 self
1092 }
1093
1094 pub fn ipv4(self) -> Self {
1096 self.ipv4_msg("Invalid IPv4 address")
1097 }
1098
1099 pub fn ipv4_msg(mut self, msg: impl Into<String>) -> Self {
1101 self.checks.push(StringCheck::Ipv4(msg.into()));
1102 self
1103 }
1104
1105 pub fn ipv6(self) -> Self {
1107 self.ipv6_msg("Invalid IPv6 address")
1108 }
1109
1110 pub fn ipv6_msg(mut self, msg: impl Into<String>) -> Self {
1112 self.checks.push(StringCheck::Ipv6(msg.into()));
1113 self
1114 }
1115
1116 pub fn cidr(self) -> Self {
1118 self.cidr_msg("Invalid CIDR")
1119 }
1120
1121 pub fn cidr_msg(mut self, msg: impl Into<String>) -> Self {
1123 self.checks.push(StringCheck::Cidr(msg.into()));
1124 self
1125 }
1126
1127 pub fn mac(self) -> Self {
1129 self.mac_msg("Invalid MAC address")
1130 }
1131
1132 pub fn mac_msg(mut self, msg: impl Into<String>) -> Self {
1134 self.checks.push(StringCheck::Mac(msg.into()));
1135 self
1136 }
1137
1138 pub fn hex(self) -> Self {
1140 self.hex_msg("Invalid hexadecimal string")
1141 }
1142
1143 pub fn hex_msg(mut self, msg: impl Into<String>) -> Self {
1145 self.checks.push(StringCheck::Hex(msg.into()));
1146 self
1147 }
1148
1149 pub fn credit_card(self) -> Self {
1151 self.credit_card_msg("Invalid credit card number")
1152 }
1153
1154 pub fn credit_card_msg(mut self, msg: impl Into<String>) -> Self {
1156 self.checks.push(StringCheck::CreditCard(msg.into()));
1157 self
1158 }
1159
1160 pub fn phone(self) -> Self {
1162 self.phone_msg("Invalid phone number")
1163 }
1164
1165 pub fn phone_msg(mut self, msg: impl Into<String>) -> Self {
1167 self.checks.push(StringCheck::Phone(msg.into()));
1168 self
1169 }
1170
1171 #[cfg(feature = "string-advanced")]
1173 pub fn phone_e164_strict(self) -> Self {
1174 self.phone_e164_strict_msg("Invalid E.164 phone number")
1175 }
1176
1177 #[cfg(feature = "string-advanced")]
1178 pub fn phone_e164_strict_msg(mut self, msg: impl Into<String>) -> Self {
1179 self.checks.push(StringCheck::PhoneE164(msg.into()));
1180 self
1181 }
1182
1183 pub fn semver(self) -> Self {
1185 self.semver_msg("Invalid semver")
1186 }
1187
1188 pub fn semver_msg(mut self, msg: impl Into<String>) -> Self {
1190 self.checks.push(StringCheck::Semver(msg.into()));
1191 self
1192 }
1193
1194 #[cfg(feature = "string-advanced")]
1196 pub fn semver_full(self) -> Self {
1197 self.semver_full_msg("Invalid semantic version")
1198 }
1199
1200 #[cfg(feature = "string-advanced")]
1201 pub fn semver_full_msg(mut self, msg: impl Into<String>) -> Self {
1202 self.checks.push(StringCheck::SemverFull(msg.into()));
1203 self
1204 }
1205
1206 pub fn jwt(self) -> Self {
1208 self.jwt_msg("Invalid JWT")
1209 }
1210
1211 pub fn jwt_msg(mut self, msg: impl Into<String>) -> Self {
1213 self.checks.push(StringCheck::Jwt(msg.into()));
1214 self
1215 }
1216
1217 pub fn ascii(self) -> Self {
1219 self.ascii_msg("String must contain only ASCII characters")
1220 }
1221
1222 pub fn ascii_msg(mut self, msg: impl Into<String>) -> Self {
1224 self.checks.push(StringCheck::Ascii(msg.into()));
1225 self
1226 }
1227
1228 pub fn alpha(self) -> Self {
1230 self.alpha_msg("String must contain only alphabetic characters")
1231 }
1232
1233 pub fn alpha_msg(mut self, msg: impl Into<String>) -> Self {
1235 self.checks.push(StringCheck::Alpha(msg.into()));
1236 self
1237 }
1238
1239 pub fn alphanumeric(self) -> Self {
1241 self.alphanumeric_msg("String must contain only alphanumeric characters")
1242 }
1243
1244 pub fn alphanumeric_msg(mut self, msg: impl Into<String>) -> Self {
1246 self.checks.push(StringCheck::Alphanumeric(msg.into()));
1247 self
1248 }
1249
1250 pub fn lowercase(self) -> Self {
1252 self.lowercase_msg("String must be lowercase")
1253 }
1254
1255 pub fn lowercase_msg(mut self, msg: impl Into<String>) -> Self {
1257 self.checks.push(StringCheck::Lowercase(msg.into()));
1258 self
1259 }
1260
1261 pub fn uppercase(self) -> Self {
1263 self.uppercase_msg("String must be uppercase")
1264 }
1265
1266 pub fn uppercase_msg(mut self, msg: impl Into<String>) -> Self {
1268 self.checks.push(StringCheck::Uppercase(msg.into()));
1269 self
1270 }
1271
1272 pub fn base64(self) -> Self {
1274 self.base64_msg("Invalid Base64 string")
1275 }
1276
1277 pub fn base64_msg(mut self, msg: impl Into<String>) -> Self {
1279 self.checks.push(StringCheck::Base64(msg.into()));
1280 self
1281 }
1282
1283 pub fn iso_date(self) -> Self {
1285 self.iso_date_msg("Invalid ISO date (expected YYYY-MM-DD)")
1286 }
1287
1288 pub fn iso_date_msg(mut self, msg: impl Into<String>) -> Self {
1290 self.checks.push(StringCheck::IsoDate(msg.into()));
1291 self
1292 }
1293
1294 pub fn iso_datetime(self) -> Self {
1296 self.iso_datetime_msg("Invalid ISO datetime")
1297 }
1298
1299 pub fn iso_datetime_msg(mut self, msg: impl Into<String>) -> Self {
1301 self.checks.push(StringCheck::IsoDatetime(msg.into()));
1302 self
1303 }
1304
1305 pub fn iso_time(self) -> Self {
1307 self.iso_time_msg("Invalid ISO time")
1308 }
1309
1310 pub fn iso_time_msg(mut self, msg: impl Into<String>) -> Self {
1312 self.checks.push(StringCheck::IsoTime(msg.into()));
1313 self
1314 }
1315
1316 pub fn hostname(self) -> Self {
1318 self.hostname_msg("Invalid hostname")
1319 }
1320
1321 pub fn hostname_msg(mut self, msg: impl Into<String>) -> Self {
1323 self.checks.push(StringCheck::Hostname(msg.into()));
1324 self
1325 }
1326
1327 pub fn cuid2(self) -> Self {
1329 self.cuid2_msg("Invalid CUID2")
1330 }
1331
1332 pub fn cuid2_msg(mut self, msg: impl Into<String>) -> Self {
1334 self.checks.push(StringCheck::Cuid2(msg.into()));
1335 self
1336 }
1337
1338 pub fn ulid(self) -> Self {
1340 self.ulid_msg("Invalid ULID")
1341 }
1342
1343 pub fn ulid_msg(mut self, msg: impl Into<String>) -> Self {
1345 self.checks.push(StringCheck::Ulid(msg.into()));
1346 self
1347 }
1348
1349 pub fn nanoid(self) -> Self {
1351 self.nanoid_msg("Invalid Nano ID")
1352 }
1353
1354 pub fn nanoid_msg(mut self, msg: impl Into<String>) -> Self {
1356 self.checks.push(StringCheck::Nanoid(msg.into()));
1357 self
1358 }
1359
1360 pub fn emoji(self) -> Self {
1362 self.emoji_msg("String must contain an emoji")
1363 }
1364
1365 pub fn emoji_msg(mut self, msg: impl Into<String>) -> Self {
1367 self.checks.push(StringCheck::Emoji(msg.into()));
1368 self
1369 }
1370
1371 pub fn slug(self) -> Self {
1373 self.slug_msg("Invalid slug")
1374 }
1375
1376 pub fn slug_msg(mut self, msg: impl Into<String>) -> Self {
1377 self.checks.push(StringCheck::Slug(msg.into()));
1378 self
1379 }
1380
1381 pub fn color(self) -> Self {
1383 self.color_msg("Invalid color")
1384 }
1385
1386 pub fn color_msg(mut self, msg: impl Into<String>) -> Self {
1387 self.checks.push(StringCheck::Color(msg.into()));
1388 self
1389 }
1390
1391 pub fn currency_code(self) -> Self {
1393 self.currency_code_msg("Invalid currency code")
1394 }
1395
1396 pub fn currency_code_msg(mut self, msg: impl Into<String>) -> Self {
1397 self.checks.push(StringCheck::Currency(msg.into()));
1398 self
1399 }
1400
1401 pub fn country_code(self) -> Self {
1403 self.country_code_msg("Invalid country code")
1404 }
1405
1406 pub fn country_code_msg(mut self, msg: impl Into<String>) -> Self {
1407 self.checks.push(StringCheck::CountryCode(msg.into()));
1408 self
1409 }
1410
1411 pub fn locale(self) -> Self {
1413 self.locale_msg("Invalid locale")
1414 }
1415
1416 pub fn locale_msg(mut self, msg: impl Into<String>) -> Self {
1417 self.checks.push(StringCheck::Locale(msg.into()));
1418 self
1419 }
1420
1421 pub fn cron(self) -> Self {
1423 self.cron_msg("Invalid cron expression")
1424 }
1425
1426 pub fn cron_msg(mut self, msg: impl Into<String>) -> Self {
1427 self.checks.push(StringCheck::Cron(msg.into()));
1428 self
1429 }
1430
1431 #[cfg(feature = "openapi")]
1435 pub fn to_json_schema(&self) -> serde_json::Value {
1436 let mut schema = serde_json::json!({"type": "string"});
1437 for check in &self.checks {
1438 match check {
1439 StringCheck::Min(n, _) => {
1440 schema["minLength"] = serde_json::json!(*n);
1441 }
1442 StringCheck::Max(n, _) => {
1443 schema["maxLength"] = serde_json::json!(*n);
1444 }
1445 StringCheck::Len(n, _) => {
1446 schema["minLength"] = serde_json::json!(*n);
1447 schema["maxLength"] = serde_json::json!(*n);
1448 }
1449 StringCheck::Email(_) => {
1450 schema["format"] = serde_json::json!("email");
1451 }
1452 StringCheck::Url(_) => {
1453 schema["format"] = serde_json::json!("uri");
1454 }
1455 #[cfg(feature = "string-advanced")]
1456 StringCheck::UrlStrict(_) => {
1457 schema["format"] = serde_json::json!("url-strict");
1458 }
1459 #[cfg(feature = "string-advanced")]
1460 StringCheck::Uri(_) => {
1461 schema["format"] = serde_json::json!("uri");
1462 }
1463 StringCheck::Uuid(_) => {
1464 schema["format"] = serde_json::json!("uuid");
1465 }
1466 #[cfg(feature = "string-advanced")]
1467 StringCheck::UuidV1(_) => {
1468 schema["format"] = serde_json::json!("uuid-v1");
1469 }
1470 #[cfg(feature = "string-advanced")]
1471 StringCheck::UuidV4(_) => {
1472 schema["format"] = serde_json::json!("uuid-v4");
1473 }
1474 #[cfg(feature = "string-advanced")]
1475 StringCheck::UuidV7(_) => {
1476 schema["format"] = serde_json::json!("uuid-v7");
1477 }
1478 StringCheck::Ip(_) => {
1479 schema["format"] = serde_json::json!("ip");
1480 }
1481 StringCheck::Slug(_) => {
1482 schema["format"] = serde_json::json!("slug");
1483 }
1484 StringCheck::Color(_) => {
1485 schema["format"] = serde_json::json!("color");
1486 }
1487 StringCheck::Currency(_) => {
1488 schema["format"] = serde_json::json!("currency-code");
1489 }
1490 StringCheck::CountryCode(_) => {
1491 schema["format"] = serde_json::json!("country-code");
1492 }
1493 StringCheck::Locale(_) => {
1494 schema["format"] = serde_json::json!("locale");
1495 }
1496 StringCheck::Cron(_) => {
1497 schema["format"] = serde_json::json!("cron");
1498 }
1499 StringCheck::Ipv4(_) => {
1500 schema["format"] = serde_json::json!("ipv4");
1501 }
1502 StringCheck::Ipv6(_) => {
1503 schema["format"] = serde_json::json!("ipv6");
1504 }
1505 StringCheck::Cidr(_) => {
1506 schema["format"] = serde_json::json!("cidr");
1507 }
1508 StringCheck::Mac(_) => {
1509 schema["format"] = serde_json::json!("mac");
1510 }
1511 StringCheck::Hex(_) => {
1512 schema["format"] = serde_json::json!("hex");
1513 }
1514 StringCheck::CreditCard(_) => {
1515 schema["format"] = serde_json::json!("credit-card");
1516 }
1517 StringCheck::Phone(_) => {
1518 schema["format"] = serde_json::json!("phone");
1519 }
1520 #[cfg(feature = "string-advanced")]
1521 StringCheck::PhoneE164(_) => {
1522 schema["format"] = serde_json::json!("phone-e164");
1523 }
1524 StringCheck::Semver(_) => {
1525 schema["format"] = serde_json::json!("semver");
1526 }
1527 #[cfg(feature = "string-advanced")]
1528 StringCheck::SemverFull(_) => {
1529 schema["format"] = serde_json::json!("semver-full");
1530 }
1531 StringCheck::Jwt(_) => {
1532 schema["format"] = serde_json::json!("jwt");
1533 }
1534 StringCheck::Ascii(_) => {
1535 schema["format"] = serde_json::json!("ascii");
1536 }
1537 StringCheck::Alpha(_) => {
1538 schema["format"] = serde_json::json!("alpha");
1539 }
1540 StringCheck::Alphanumeric(_) => {
1541 schema["format"] = serde_json::json!("alphanumeric");
1542 }
1543 StringCheck::Lowercase(_) => {
1544 schema["format"] = serde_json::json!("lowercase");
1545 }
1546 StringCheck::Uppercase(_) => {
1547 schema["format"] = serde_json::json!("uppercase");
1548 }
1549 StringCheck::IsoDate(_) => {
1550 schema["format"] = serde_json::json!("date");
1551 }
1552 StringCheck::IsoDatetime(_) => {
1553 schema["format"] = serde_json::json!("date-time");
1554 }
1555 StringCheck::IsoTime(_) => {
1556 schema["format"] = serde_json::json!("time");
1557 }
1558 StringCheck::Hostname(_) => {
1559 schema["format"] = serde_json::json!("hostname");
1560 }
1561 StringCheck::NonEmpty(_) => {
1562 schema["minLength"] = serde_json::json!(1);
1563 }
1564 StringCheck::Cuid2(_) => {
1565 schema["format"] = serde_json::json!("cuid2");
1566 }
1567 StringCheck::Ulid(_) => {
1568 schema["format"] = serde_json::json!("ulid");
1569 }
1570 StringCheck::Nanoid(_) => {
1571 schema["format"] = serde_json::json!("nanoid");
1572 }
1573 StringCheck::Emoji(_) => {
1574 schema["format"] = serde_json::json!("emoji");
1575 }
1576 _ => {}
1577 }
1578 }
1579 schema
1580 }
1581}
1582
1583impl Default for ZString {
1584 fn default() -> Self {
1585 Self::new()
1586 }
1587}
1588
1589impl VldSchema for ZString {
1590 type Output = String;
1591
1592 fn parse_value(&self, value: &Value) -> Result<String, VldError> {
1593 let type_err = |value: &Value| -> VldError {
1594 let msg = self
1595 .custom_type_error
1596 .clone()
1597 .unwrap_or_else(|| format!("Expected string, received {}", value_type_name(value)));
1598 VldError::single_with_value(
1599 IssueCode::InvalidType {
1600 expected: "string".to_string(),
1601 received: value_type_name(value),
1602 },
1603 msg,
1604 value,
1605 )
1606 };
1607
1608 let mut s = if let Some(s) = value.as_str() {
1610 s.to_string()
1611 } else if self.coerce {
1612 match value {
1613 Value::Number(n) => n.to_string(),
1614 Value::Bool(b) => b.to_string(),
1615 _ => return Err(type_err(value)),
1616 }
1617 } else {
1618 return Err(type_err(value));
1619 };
1620
1621 for t in &self.transforms {
1623 match t {
1624 StringTransform::Trim => s = s.trim().to_string(),
1625 StringTransform::ToLowerCase => s = s.to_lowercase(),
1626 StringTransform::ToUpperCase => s = s.to_uppercase(),
1627 }
1628 }
1629
1630 let str_val = Value::String(s.clone());
1632 let mut errors = VldError::new();
1633
1634 for check in &self.checks {
1635 match check {
1636 StringCheck::Min(min, msg) => {
1637 if s.chars().count() < *min {
1638 errors.push_with_value(
1639 IssueCode::TooSmall {
1640 minimum: *min as f64,
1641 inclusive: true,
1642 },
1643 msg.clone(),
1644 &str_val,
1645 );
1646 }
1647 }
1648 StringCheck::Max(max, msg) => {
1649 if s.chars().count() > *max {
1650 errors.push_with_value(
1651 IssueCode::TooBig {
1652 maximum: *max as f64,
1653 inclusive: true,
1654 },
1655 msg.clone(),
1656 &str_val,
1657 );
1658 }
1659 }
1660 StringCheck::Len(len, msg) => {
1661 if s.chars().count() != *len {
1662 errors.push_with_value(
1663 IssueCode::Custom {
1664 code: "invalid_length".to_string(),
1665 },
1666 msg.clone(),
1667 &str_val,
1668 );
1669 }
1670 }
1671 StringCheck::Email(msg) => {
1672 if !is_valid_email(&s) {
1673 errors.push_with_value(
1674 IssueCode::InvalidString {
1675 validation: StringValidation::Email,
1676 },
1677 msg.clone(),
1678 &str_val,
1679 );
1680 }
1681 }
1682 StringCheck::Url(msg) => {
1683 if !is_valid_url(&s) {
1684 errors.push_with_value(
1685 IssueCode::InvalidString {
1686 validation: StringValidation::Url,
1687 },
1688 msg.clone(),
1689 &str_val,
1690 );
1691 }
1692 }
1693 #[cfg(feature = "string-advanced")]
1694 StringCheck::UrlStrict(msg) => {
1695 if !is_valid_url_strict(&s) {
1696 errors.push_with_value(
1697 IssueCode::InvalidString {
1698 validation: StringValidation::UrlStrict,
1699 },
1700 msg.clone(),
1701 &str_val,
1702 );
1703 }
1704 }
1705 #[cfg(feature = "string-advanced")]
1706 StringCheck::Uri(msg) => {
1707 if !is_valid_uri(&s) {
1708 errors.push_with_value(
1709 IssueCode::InvalidString {
1710 validation: StringValidation::Uri,
1711 },
1712 msg.clone(),
1713 &str_val,
1714 );
1715 }
1716 }
1717 StringCheck::Uuid(msg) => {
1718 if !is_valid_uuid(&s) {
1719 errors.push_with_value(
1720 IssueCode::InvalidString {
1721 validation: StringValidation::Uuid,
1722 },
1723 msg.clone(),
1724 &str_val,
1725 );
1726 }
1727 }
1728 #[cfg(feature = "string-advanced")]
1729 StringCheck::UuidV1(msg) => {
1730 if !is_valid_uuid_version(&s, 1) {
1731 errors.push_with_value(
1732 IssueCode::InvalidString {
1733 validation: StringValidation::UuidV1,
1734 },
1735 msg.clone(),
1736 &str_val,
1737 );
1738 }
1739 }
1740 #[cfg(feature = "string-advanced")]
1741 StringCheck::UuidV4(msg) => {
1742 if !is_valid_uuid_version(&s, 4) {
1743 errors.push_with_value(
1744 IssueCode::InvalidString {
1745 validation: StringValidation::UuidV4,
1746 },
1747 msg.clone(),
1748 &str_val,
1749 );
1750 }
1751 }
1752 #[cfg(feature = "string-advanced")]
1753 StringCheck::UuidV7(msg) => {
1754 if !is_valid_uuid_version(&s, 7) {
1755 errors.push_with_value(
1756 IssueCode::InvalidString {
1757 validation: StringValidation::UuidV7,
1758 },
1759 msg.clone(),
1760 &str_val,
1761 );
1762 }
1763 }
1764 StringCheck::Ip(msg) => {
1765 if !is_valid_ip(&s) {
1766 errors.push_with_value(
1767 IssueCode::InvalidString {
1768 validation: StringValidation::Ip,
1769 },
1770 msg.clone(),
1771 &str_val,
1772 );
1773 }
1774 }
1775 StringCheck::Slug(msg) => {
1776 if !is_valid_slug(&s) {
1777 errors.push_with_value(
1778 IssueCode::InvalidString {
1779 validation: StringValidation::Slug,
1780 },
1781 msg.clone(),
1782 &str_val,
1783 );
1784 }
1785 }
1786 StringCheck::Color(msg) => {
1787 if !is_valid_color(&s) {
1788 errors.push_with_value(
1789 IssueCode::InvalidString {
1790 validation: StringValidation::Color,
1791 },
1792 msg.clone(),
1793 &str_val,
1794 );
1795 }
1796 }
1797 StringCheck::Currency(msg) => {
1798 if !is_valid_currency_code(&s) {
1799 errors.push_with_value(
1800 IssueCode::InvalidString {
1801 validation: StringValidation::Currency,
1802 },
1803 msg.clone(),
1804 &str_val,
1805 );
1806 }
1807 }
1808 StringCheck::CountryCode(msg) => {
1809 if !is_valid_country_code(&s) {
1810 errors.push_with_value(
1811 IssueCode::InvalidString {
1812 validation: StringValidation::CountryCode,
1813 },
1814 msg.clone(),
1815 &str_val,
1816 );
1817 }
1818 }
1819 StringCheck::Locale(msg) => {
1820 if !is_valid_locale(&s) {
1821 errors.push_with_value(
1822 IssueCode::InvalidString {
1823 validation: StringValidation::Locale,
1824 },
1825 msg.clone(),
1826 &str_val,
1827 );
1828 }
1829 }
1830 StringCheck::Cron(msg) => {
1831 if !is_valid_cron(&s) {
1832 errors.push_with_value(
1833 IssueCode::InvalidString {
1834 validation: StringValidation::Cron,
1835 },
1836 msg.clone(),
1837 &str_val,
1838 );
1839 }
1840 }
1841 #[cfg(feature = "regex")]
1842 StringCheck::Regex(re, msg) => {
1843 if !re.is_match(&s) {
1844 errors.push_with_value(
1845 IssueCode::InvalidString {
1846 validation: StringValidation::Regex,
1847 },
1848 msg.clone(),
1849 &str_val,
1850 );
1851 }
1852 }
1853 StringCheck::StartsWith(prefix, msg) => {
1854 if !s.starts_with(prefix.as_str()) {
1855 errors.push_with_value(
1856 IssueCode::InvalidString {
1857 validation: StringValidation::StartsWith,
1858 },
1859 msg.clone(),
1860 &str_val,
1861 );
1862 }
1863 }
1864 StringCheck::EndsWith(suffix, msg) => {
1865 if !s.ends_with(suffix.as_str()) {
1866 errors.push_with_value(
1867 IssueCode::InvalidString {
1868 validation: StringValidation::EndsWith,
1869 },
1870 msg.clone(),
1871 &str_val,
1872 );
1873 }
1874 }
1875 StringCheck::Contains(sub, msg) => {
1876 if !s.contains(sub.as_str()) {
1877 errors.push_with_value(
1878 IssueCode::Custom {
1879 code: "invalid_string".to_string(),
1880 },
1881 msg.clone(),
1882 &str_val,
1883 );
1884 }
1885 }
1886 StringCheck::NonEmpty(msg) => {
1887 if s.is_empty() {
1888 errors.push_with_value(
1889 IssueCode::TooSmall {
1890 minimum: 1.0,
1891 inclusive: true,
1892 },
1893 msg.clone(),
1894 &str_val,
1895 );
1896 }
1897 }
1898 StringCheck::Ipv4(msg) => {
1899 if !is_valid_ipv4(&s) {
1900 errors.push_with_value(
1901 IssueCode::InvalidString {
1902 validation: StringValidation::Ipv4,
1903 },
1904 msg.clone(),
1905 &str_val,
1906 );
1907 }
1908 }
1909 StringCheck::Ipv6(msg) => {
1910 if !is_valid_ipv6(&s) {
1911 errors.push_with_value(
1912 IssueCode::InvalidString {
1913 validation: StringValidation::Ipv6,
1914 },
1915 msg.clone(),
1916 &str_val,
1917 );
1918 }
1919 }
1920 StringCheck::Cidr(msg) => {
1921 if !is_valid_cidr(&s) {
1922 errors.push_with_value(
1923 IssueCode::InvalidString {
1924 validation: StringValidation::Cidr,
1925 },
1926 msg.clone(),
1927 &str_val,
1928 );
1929 }
1930 }
1931 StringCheck::Mac(msg) => {
1932 if !is_valid_mac(&s) {
1933 errors.push_with_value(
1934 IssueCode::InvalidString {
1935 validation: StringValidation::Mac,
1936 },
1937 msg.clone(),
1938 &str_val,
1939 );
1940 }
1941 }
1942 StringCheck::Hex(msg) => {
1943 if !is_valid_hex(&s) {
1944 errors.push_with_value(
1945 IssueCode::InvalidString {
1946 validation: StringValidation::Hex,
1947 },
1948 msg.clone(),
1949 &str_val,
1950 );
1951 }
1952 }
1953 StringCheck::CreditCard(msg) => {
1954 if !is_valid_credit_card(&s) {
1955 errors.push_with_value(
1956 IssueCode::InvalidString {
1957 validation: StringValidation::CreditCard,
1958 },
1959 msg.clone(),
1960 &str_val,
1961 );
1962 }
1963 }
1964 StringCheck::Phone(msg) => {
1965 if !is_valid_phone(&s) {
1966 errors.push_with_value(
1967 IssueCode::InvalidString {
1968 validation: StringValidation::Phone,
1969 },
1970 msg.clone(),
1971 &str_val,
1972 );
1973 }
1974 }
1975 #[cfg(feature = "string-advanced")]
1976 StringCheck::PhoneE164(msg) => {
1977 if !is_valid_phone_e164_strict(&s) {
1978 errors.push_with_value(
1979 IssueCode::InvalidString {
1980 validation: StringValidation::PhoneE164,
1981 },
1982 msg.clone(),
1983 &str_val,
1984 );
1985 }
1986 }
1987 StringCheck::Semver(msg) => {
1988 if !is_valid_semver(&s) {
1989 errors.push_with_value(
1990 IssueCode::InvalidString {
1991 validation: StringValidation::Semver,
1992 },
1993 msg.clone(),
1994 &str_val,
1995 );
1996 }
1997 }
1998 #[cfg(feature = "string-advanced")]
1999 StringCheck::SemverFull(msg) => {
2000 if !is_valid_semver_full(&s) {
2001 errors.push_with_value(
2002 IssueCode::InvalidString {
2003 validation: StringValidation::SemverFull,
2004 },
2005 msg.clone(),
2006 &str_val,
2007 );
2008 }
2009 }
2010 StringCheck::Jwt(msg) => {
2011 if !is_valid_jwt(&s) {
2012 errors.push_with_value(
2013 IssueCode::InvalidString {
2014 validation: StringValidation::Jwt,
2015 },
2016 msg.clone(),
2017 &str_val,
2018 );
2019 }
2020 }
2021 StringCheck::Ascii(msg) => {
2022 if !s.is_ascii() {
2023 errors.push_with_value(
2024 IssueCode::InvalidString {
2025 validation: StringValidation::Ascii,
2026 },
2027 msg.clone(),
2028 &str_val,
2029 );
2030 }
2031 }
2032 StringCheck::Alpha(msg) => {
2033 if !s.chars().all(|c| c.is_ascii_alphabetic()) {
2034 errors.push_with_value(
2035 IssueCode::InvalidString {
2036 validation: StringValidation::Alpha,
2037 },
2038 msg.clone(),
2039 &str_val,
2040 );
2041 }
2042 }
2043 StringCheck::Alphanumeric(msg) => {
2044 if !s.chars().all(|c| c.is_ascii_alphanumeric()) {
2045 errors.push_with_value(
2046 IssueCode::InvalidString {
2047 validation: StringValidation::Alphanumeric,
2048 },
2049 msg.clone(),
2050 &str_val,
2051 );
2052 }
2053 }
2054 StringCheck::Lowercase(msg) => {
2055 if s != s.to_lowercase() {
2056 errors.push_with_value(
2057 IssueCode::InvalidString {
2058 validation: StringValidation::Lowercase,
2059 },
2060 msg.clone(),
2061 &str_val,
2062 );
2063 }
2064 }
2065 StringCheck::Uppercase(msg) => {
2066 if s != s.to_uppercase() {
2067 errors.push_with_value(
2068 IssueCode::InvalidString {
2069 validation: StringValidation::Uppercase,
2070 },
2071 msg.clone(),
2072 &str_val,
2073 );
2074 }
2075 }
2076 StringCheck::Base64(msg) => {
2077 if !is_valid_base64(&s) {
2078 errors.push_with_value(
2079 IssueCode::InvalidString {
2080 validation: StringValidation::Base64,
2081 },
2082 msg.clone(),
2083 &str_val,
2084 );
2085 }
2086 }
2087 StringCheck::IsoDate(msg) => {
2088 if !is_valid_iso_date(&s) {
2089 errors.push_with_value(
2090 IssueCode::InvalidString {
2091 validation: StringValidation::IsoDate,
2092 },
2093 msg.clone(),
2094 &str_val,
2095 );
2096 }
2097 }
2098 StringCheck::IsoDatetime(msg) => {
2099 if !is_valid_iso_datetime(&s) {
2100 errors.push_with_value(
2101 IssueCode::InvalidString {
2102 validation: StringValidation::IsoDatetime,
2103 },
2104 msg.clone(),
2105 &str_val,
2106 );
2107 }
2108 }
2109 StringCheck::IsoTime(msg) => {
2110 if !is_valid_iso_time(&s) {
2111 errors.push_with_value(
2112 IssueCode::InvalidString {
2113 validation: StringValidation::IsoTime,
2114 },
2115 msg.clone(),
2116 &str_val,
2117 );
2118 }
2119 }
2120 StringCheck::Hostname(msg) => {
2121 if !is_valid_hostname(&s) {
2122 errors.push_with_value(
2123 IssueCode::InvalidString {
2124 validation: StringValidation::Hostname,
2125 },
2126 msg.clone(),
2127 &str_val,
2128 );
2129 }
2130 }
2131 StringCheck::Cuid2(msg) => {
2132 if !is_valid_cuid2(&s) {
2133 errors.push_with_value(
2134 IssueCode::InvalidString {
2135 validation: StringValidation::Cuid2,
2136 },
2137 msg.clone(),
2138 &str_val,
2139 );
2140 }
2141 }
2142 StringCheck::Ulid(msg) => {
2143 if !is_valid_ulid(&s) {
2144 errors.push_with_value(
2145 IssueCode::InvalidString {
2146 validation: StringValidation::Ulid,
2147 },
2148 msg.clone(),
2149 &str_val,
2150 );
2151 }
2152 }
2153 StringCheck::Nanoid(msg) => {
2154 if !is_valid_nanoid(&s) {
2155 errors.push_with_value(
2156 IssueCode::InvalidString {
2157 validation: StringValidation::Nanoid,
2158 },
2159 msg.clone(),
2160 &str_val,
2161 );
2162 }
2163 }
2164 StringCheck::Emoji(msg) => {
2165 if !is_valid_emoji(&s) {
2166 errors.push_with_value(
2167 IssueCode::InvalidString {
2168 validation: StringValidation::Emoji,
2169 },
2170 msg.clone(),
2171 &str_val,
2172 );
2173 }
2174 }
2175 }
2176 }
2177
2178 if errors.is_empty() {
2179 Ok(s)
2180 } else {
2181 Err(errors)
2182 }
2183 }
2184}