Skip to main content

vld/primitives/
string.rs

1use serde_json::Value;
2
3use crate::error::{value_type_name, IssueCode, StringValidation, VldError};
4use crate::schema::VldSchema;
5
6// ---------------------------------------------------------------------------
7// Manual validation functions (no regex)
8// ---------------------------------------------------------------------------
9
10fn is_valid_email(s: &str) -> bool {
11    // local@domain — basic RFC-like check
12    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    // Local part: printable ASCII except some specials, no spaces
24    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    // 8-4-4-4-12 hex with dashes: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
36    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    // Must start with http:// or https:// and have something after
68    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    // No whitespace allowed
79    !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; // fe80::/10
108                let is_unique_local = (seg0 & 0xfe00) == 0xfc00; // fc00::/7
109                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        // No leading zeros (except "0" itself)
133        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    // Handle :: shorthand
146    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    // E.164: max 15 digits, min practical 8.
264    (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    // At most 2 padding chars
406    let pad_count = s.bytes().rev().take_while(|&b| b == b'=').count();
407    pad_count <= 2
408}
409
410/// Parse exactly `n` ASCII digits from `s`, return the number and remaining slice.
411fn 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    // YYYY-MM-DD (exactly 10 chars)
424    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; // any 4-digit year is OK
445    (1..=12).contains(&month) && (1..=31).contains(&day)
446}
447
448fn is_valid_iso_time(s: &str) -> bool {
449    // HH:MM[:SS[.frac]]
450    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    // Optional :SS
472    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    // Optional .fraction
487    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    // YYYY-MM-DDTHH:MM[:SS[.frac]][Z|+HH:MM|-HH:MM]
496    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    // Find timezone suffix
508    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        // skip first char to avoid matching negative in time itself
514        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    // Crockford's Base32: 0-9 A-H J K M N P-T V-Z (no I, L, O, U)
557    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// ---------------------------------------------------------------------------
594// StringCheck / StringTransform
595// ---------------------------------------------------------------------------
596
597#[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    /// Stable key identifying the check category.
659    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    /// Replace the error message stored in this check.
721    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/// Schema for string validation. Created via [`vld::string()`](crate::string).
785///
786/// # Example
787/// ```
788/// use vld::prelude::*;
789///
790/// let schema = vld::string().min(3).max(20).email();
791/// ```
792#[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    /// Set a custom error message for type mismatch (when the input is not a string).
811    ///
812    /// # Example
813    /// ```
814    /// use vld::prelude::*;
815    /// let schema = vld::string().type_error("Must be text!");
816    /// let err = schema.parse("42").unwrap_err();
817    /// assert!(err.issues[0].message.contains("Must be text!"));
818    /// ```
819    pub fn type_error(mut self, msg: impl Into<String>) -> Self {
820        self.custom_type_error = Some(msg.into());
821        self
822    }
823
824    /// Override error messages in bulk by check key.
825    ///
826    /// The closure receives the check key (e.g. `"too_small"`, `"invalid_email"`)
827    /// and should return `Some(new_message)` to replace, or `None` to keep the original.
828    ///
829    /// Available keys: `"too_small"`, `"too_big"`, `"invalid_length"`, `"invalid_email"`,
830    /// `"invalid_url"`, `"invalid_uuid"`, `"invalid_ip"`, `"invalid_regex"`,
831    /// `"invalid_starts_with"`, `"invalid_ends_with"`, `"invalid_contains"`, `"non_empty"`,
832    /// `"invalid_ipv4"`, `"invalid_ipv6"`, `"invalid_cidr"`, `"invalid_mac"`, `"invalid_hex"`,
833    /// `"invalid_credit_card"`, `"invalid_phone"`, `"invalid_semver"`, `"invalid_jwt"`,
834    /// `"invalid_ascii"`, `"invalid_alpha"`, `"invalid_alphanumeric"`, `"invalid_lowercase"`,
835    /// `"invalid_uppercase"`, `"invalid_base64"`, `"invalid_iso_date"`,
836    /// `"invalid_iso_datetime"`, `"invalid_iso_time"`, `"invalid_hostname"`,
837    /// `"invalid_cuid2"`, `"invalid_ulid"`, `"invalid_nanoid"`, `"invalid_emoji"`.
838    ///
839    /// # Example
840    /// ```
841    /// use vld::prelude::*;
842    /// let schema = vld::string().min(3).email()
843    ///     .with_messages(|key| match key {
844    ///         "too_small" => Some("Too short!".into()),
845    ///         "invalid_email" => Some("Bad email!".into()),
846    ///         _ => None,
847    ///     });
848    /// ```
849    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    /// Minimum string length (inclusive).
862    pub fn min(self, len: usize) -> Self {
863        self.min_msg(len, format!("String must be at least {} characters", len))
864    }
865
866    /// Minimum string length with custom message.
867    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    /// Maximum string length (inclusive).
873    pub fn max(self, len: usize) -> Self {
874        self.max_msg(len, format!("String must be at most {} characters", len))
875    }
876
877    /// Maximum string length with custom message.
878    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    /// Exact string length.
884    pub fn len(self, len: usize) -> Self {
885        self.len_msg(len, format!("String must be exactly {} characters", len))
886    }
887
888    /// Exact string length with custom message.
889    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    /// Must be a valid email address.
895    pub fn email(self) -> Self {
896        self.email_msg("Invalid email address")
897    }
898
899    /// Must be a valid email address, with custom message.
900    pub fn email_msg(mut self, msg: impl Into<String>) -> Self {
901        self.checks.push(StringCheck::Email(msg.into()));
902        self
903    }
904
905    /// Must be a valid URL (http/https).
906    pub fn url(self) -> Self {
907        self.url_msg("Invalid URL")
908    }
909
910    /// Must be a valid URL, with custom message.
911    pub fn url_msg(mut self, msg: impl Into<String>) -> Self {
912        self.checks.push(StringCheck::Url(msg.into()));
913        self
914    }
915
916    /// Must be a strict URL (http/https + valid host).
917    #[cfg(feature = "string-advanced")]
918    pub fn url_strict(self) -> Self {
919        self.url_strict_msg("Invalid strict URL")
920    }
921
922    /// Must be a strict URL, with custom message.
923    #[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    /// Must be a valid URI.
930    #[cfg(feature = "string-advanced")]
931    pub fn uri(self) -> Self {
932        self.uri_msg("Invalid URI")
933    }
934
935    /// Must be a valid URI, with custom message.
936    #[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    /// Must be a valid UUID.
943    pub fn uuid(self) -> Self {
944        self.uuid_msg("Invalid UUID")
945    }
946
947    /// Must be a valid UUID, with custom message.
948    pub fn uuid_msg(mut self, msg: impl Into<String>) -> Self {
949        self.checks.push(StringCheck::Uuid(msg.into()));
950        self
951    }
952
953    /// Must be a valid UUID v1.
954    #[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    /// Must be a valid UUID v4.
966    #[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    /// Must be a valid UUID v7.
978    #[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    /// Must be a valid IP address (IPv4 or IPv6).
990    pub fn ip(self) -> Self {
991        self.ip_msg("Invalid IP address")
992    }
993
994    /// Must be a valid IP address (IPv4 or IPv6), with custom message.
995    pub fn ip_msg(mut self, msg: impl Into<String>) -> Self {
996        self.checks.push(StringCheck::Ip(msg.into()));
997        self
998    }
999
1000    /// Must match the given regex.
1001    ///
1002    /// Requires the `regex` feature.
1003    #[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    /// Must match the given regex, with custom message.
1009    ///
1010    /// Requires the `regex` feature.
1011    #[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    /// Must start with the given prefix.
1018    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    /// Must start with the given prefix, with custom message.
1025    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    /// Must end with the given suffix.
1032    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    /// Must end with the given suffix, with custom message.
1039    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    /// Must contain the given substring.
1046    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    /// Must contain the given substring, with custom message.
1053    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    /// Must not be empty.
1060    pub fn non_empty(self) -> Self {
1061        self.non_empty_msg("String must not be empty")
1062    }
1063
1064    /// Must not be empty, with custom message.
1065    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    /// Trim whitespace before validation.
1071    pub fn trim(mut self) -> Self {
1072        self.transforms.push(StringTransform::Trim);
1073        self
1074    }
1075
1076    /// Convert to lowercase before validation.
1077    pub fn to_lowercase(mut self) -> Self {
1078        self.transforms.push(StringTransform::ToLowerCase);
1079        self
1080    }
1081
1082    /// Convert to uppercase before validation.
1083    pub fn to_uppercase(mut self) -> Self {
1084        self.transforms.push(StringTransform::ToUpperCase);
1085        self
1086    }
1087
1088    /// Coerce non-string values (numbers, booleans) to string.
1089    pub fn coerce(mut self) -> Self {
1090        self.coerce = true;
1091        self
1092    }
1093
1094    /// Must be a valid IPv4 address.
1095    pub fn ipv4(self) -> Self {
1096        self.ipv4_msg("Invalid IPv4 address")
1097    }
1098
1099    /// Must be a valid IPv4 address, with custom message.
1100    pub fn ipv4_msg(mut self, msg: impl Into<String>) -> Self {
1101        self.checks.push(StringCheck::Ipv4(msg.into()));
1102        self
1103    }
1104
1105    /// Must be a valid IPv6 address.
1106    pub fn ipv6(self) -> Self {
1107        self.ipv6_msg("Invalid IPv6 address")
1108    }
1109
1110    /// Must be a valid IPv6 address, with custom message.
1111    pub fn ipv6_msg(mut self, msg: impl Into<String>) -> Self {
1112        self.checks.push(StringCheck::Ipv6(msg.into()));
1113        self
1114    }
1115
1116    /// Must be a valid CIDR block (IPv4/IPv6 with prefix).
1117    pub fn cidr(self) -> Self {
1118        self.cidr_msg("Invalid CIDR")
1119    }
1120
1121    /// Must be a valid CIDR block, with custom message.
1122    pub fn cidr_msg(mut self, msg: impl Into<String>) -> Self {
1123        self.checks.push(StringCheck::Cidr(msg.into()));
1124        self
1125    }
1126
1127    /// Must be a valid MAC address (`xx:xx:xx:xx:xx:xx`).
1128    pub fn mac(self) -> Self {
1129        self.mac_msg("Invalid MAC address")
1130    }
1131
1132    /// Must be a valid MAC address, with custom message.
1133    pub fn mac_msg(mut self, msg: impl Into<String>) -> Self {
1134        self.checks.push(StringCheck::Mac(msg.into()));
1135        self
1136    }
1137
1138    /// Must contain only hexadecimal characters.
1139    pub fn hex(self) -> Self {
1140        self.hex_msg("Invalid hexadecimal string")
1141    }
1142
1143    /// Must contain only hexadecimal characters, with custom message.
1144    pub fn hex_msg(mut self, msg: impl Into<String>) -> Self {
1145        self.checks.push(StringCheck::Hex(msg.into()));
1146        self
1147    }
1148
1149    /// Must be a valid credit card number (Luhn check).
1150    pub fn credit_card(self) -> Self {
1151        self.credit_card_msg("Invalid credit card number")
1152    }
1153
1154    /// Must be a valid credit card number, with custom message.
1155    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    /// Must be a valid international phone number (E.164-like, starts with `+`).
1161    pub fn phone(self) -> Self {
1162        self.phone_msg("Invalid phone number")
1163    }
1164
1165    /// Must be a valid international phone number, with custom message.
1166    pub fn phone_msg(mut self, msg: impl Into<String>) -> Self {
1167        self.checks.push(StringCheck::Phone(msg.into()));
1168        self
1169    }
1170
1171    /// Must be a valid strict E.164 phone number.
1172    #[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    /// Must be a valid semantic version (`MAJOR.MINOR.PATCH`, optional `v` prefix).
1184    pub fn semver(self) -> Self {
1185        self.semver_msg("Invalid semver")
1186    }
1187
1188    /// Must be a valid semantic version, with custom message.
1189    pub fn semver_msg(mut self, msg: impl Into<String>) -> Self {
1190        self.checks.push(StringCheck::Semver(msg.into()));
1191        self
1192    }
1193
1194    /// Must be a full semantic version parsed by the `semver` crate.
1195    #[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    /// Must be a JWT-like token (`header.payload.signature`, base64url parts).
1207    pub fn jwt(self) -> Self {
1208        self.jwt_msg("Invalid JWT")
1209    }
1210
1211    /// Must be a JWT-like token, with custom message.
1212    pub fn jwt_msg(mut self, msg: impl Into<String>) -> Self {
1213        self.checks.push(StringCheck::Jwt(msg.into()));
1214        self
1215    }
1216
1217    /// Must contain only ASCII characters.
1218    pub fn ascii(self) -> Self {
1219        self.ascii_msg("String must contain only ASCII characters")
1220    }
1221
1222    /// Must contain only ASCII characters, with custom message.
1223    pub fn ascii_msg(mut self, msg: impl Into<String>) -> Self {
1224        self.checks.push(StringCheck::Ascii(msg.into()));
1225        self
1226    }
1227
1228    /// Must contain only alphabetic ASCII characters.
1229    pub fn alpha(self) -> Self {
1230        self.alpha_msg("String must contain only alphabetic characters")
1231    }
1232
1233    /// Must contain only alphabetic ASCII characters, with custom message.
1234    pub fn alpha_msg(mut self, msg: impl Into<String>) -> Self {
1235        self.checks.push(StringCheck::Alpha(msg.into()));
1236        self
1237    }
1238
1239    /// Must contain only ASCII alphanumeric characters.
1240    pub fn alphanumeric(self) -> Self {
1241        self.alphanumeric_msg("String must contain only alphanumeric characters")
1242    }
1243
1244    /// Must contain only ASCII alphanumeric characters, with custom message.
1245    pub fn alphanumeric_msg(mut self, msg: impl Into<String>) -> Self {
1246        self.checks.push(StringCheck::Alphanumeric(msg.into()));
1247        self
1248    }
1249
1250    /// Must already be lowercase.
1251    pub fn lowercase(self) -> Self {
1252        self.lowercase_msg("String must be lowercase")
1253    }
1254
1255    /// Must already be lowercase, with custom message.
1256    pub fn lowercase_msg(mut self, msg: impl Into<String>) -> Self {
1257        self.checks.push(StringCheck::Lowercase(msg.into()));
1258        self
1259    }
1260
1261    /// Must already be uppercase.
1262    pub fn uppercase(self) -> Self {
1263        self.uppercase_msg("String must be uppercase")
1264    }
1265
1266    /// Must already be uppercase, with custom message.
1267    pub fn uppercase_msg(mut self, msg: impl Into<String>) -> Self {
1268        self.checks.push(StringCheck::Uppercase(msg.into()));
1269        self
1270    }
1271
1272    /// Must be a valid Base64 string.
1273    pub fn base64(self) -> Self {
1274        self.base64_msg("Invalid Base64 string")
1275    }
1276
1277    /// Must be a valid Base64 string, with custom message.
1278    pub fn base64_msg(mut self, msg: impl Into<String>) -> Self {
1279        self.checks.push(StringCheck::Base64(msg.into()));
1280        self
1281    }
1282
1283    /// Must be a valid ISO 8601 date (YYYY-MM-DD).
1284    pub fn iso_date(self) -> Self {
1285        self.iso_date_msg("Invalid ISO date (expected YYYY-MM-DD)")
1286    }
1287
1288    /// Must be a valid ISO 8601 date, with custom message.
1289    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    /// Must be a valid ISO 8601 datetime.
1295    pub fn iso_datetime(self) -> Self {
1296        self.iso_datetime_msg("Invalid ISO datetime")
1297    }
1298
1299    /// Must be a valid ISO 8601 datetime, with custom message.
1300    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    /// Must be a valid ISO 8601 time (HH:MM or HH:MM:SS).
1306    pub fn iso_time(self) -> Self {
1307        self.iso_time_msg("Invalid ISO time")
1308    }
1309
1310    /// Must be a valid ISO 8601 time, with custom message.
1311    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    /// Must be a valid hostname.
1317    pub fn hostname(self) -> Self {
1318        self.hostname_msg("Invalid hostname")
1319    }
1320
1321    /// Must be a valid hostname, with custom message.
1322    pub fn hostname_msg(mut self, msg: impl Into<String>) -> Self {
1323        self.checks.push(StringCheck::Hostname(msg.into()));
1324        self
1325    }
1326
1327    /// Must be a valid CUID2 string (lowercase alphanumeric, starts with a letter).
1328    pub fn cuid2(self) -> Self {
1329        self.cuid2_msg("Invalid CUID2")
1330    }
1331
1332    /// Must be a valid CUID2 string, with custom message.
1333    pub fn cuid2_msg(mut self, msg: impl Into<String>) -> Self {
1334        self.checks.push(StringCheck::Cuid2(msg.into()));
1335        self
1336    }
1337
1338    /// Must be a valid ULID (26-char Crockford Base32).
1339    pub fn ulid(self) -> Self {
1340        self.ulid_msg("Invalid ULID")
1341    }
1342
1343    /// Must be a valid ULID, with custom message.
1344    pub fn ulid_msg(mut self, msg: impl Into<String>) -> Self {
1345        self.checks.push(StringCheck::Ulid(msg.into()));
1346        self
1347    }
1348
1349    /// Must be a valid Nano ID (alphanumeric + `_` + `-`).
1350    pub fn nanoid(self) -> Self {
1351        self.nanoid_msg("Invalid Nano ID")
1352    }
1353
1354    /// Must be a valid Nano ID, with custom message.
1355    pub fn nanoid_msg(mut self, msg: impl Into<String>) -> Self {
1356        self.checks.push(StringCheck::Nanoid(msg.into()));
1357        self
1358    }
1359
1360    /// Must contain at least one emoji character.
1361    pub fn emoji(self) -> Self {
1362        self.emoji_msg("String must contain an emoji")
1363    }
1364
1365    /// Must contain an emoji, with custom message.
1366    pub fn emoji_msg(mut self, msg: impl Into<String>) -> Self {
1367        self.checks.push(StringCheck::Emoji(msg.into()));
1368        self
1369    }
1370
1371    /// Must be a URL-friendly slug (`[a-z0-9-]`).
1372    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    /// Must be a hex color (`#RRGGBB` or `#RRGGBBAA`).
1382    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    /// Must be a currency code (ISO-4217-like uppercase 3 letters).
1392    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    /// Must be a country code (ISO-3166 alpha-2).
1402    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    /// Must be a locale (`ll` or `ll-RR`, example: `en`, `en-US`).
1412    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    /// Must be a cron expression (5 or 6 fields).
1422    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    /// Generate a JSON Schema representation of this string schema.
1432    ///
1433    /// Requires the `openapi` feature.
1434    #[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        // Extract string value
1609        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        // Apply transforms
1622        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        // Run checks, accumulate errors
1631        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}