Skip to main content

tanzim_validate/
string.rs

1use crate::error::{Error, ErrorKind};
2use crate::{Meta, Validator};
3use tanzim_value::{Value, ValueType};
4
5/// (`string` feature) Accepts a string, with optional length bounds and (with the `regex` feature) a
6/// pattern. No coercion: non-string values are rejected.
7#[derive(Debug, Clone, Default)]
8pub struct Str {
9    meta: Meta,
10    min_chars: Option<usize>,
11    max_chars: Option<usize>,
12    #[cfg(feature = "regex")]
13    pattern: Option<regex::Regex>,
14}
15
16impl Str {
17    /// Attach human-facing metadata (name, description, examples, default, output conversion).
18    pub fn with_meta(mut self, meta: Meta) -> Self {
19        self.meta = meta;
20        self
21    }
22
23    pub fn new() -> Self {
24        Self::default()
25    }
26
27    pub fn min_chars(mut self, min: usize) -> Self {
28        self.min_chars = Some(min);
29        self
30    }
31
32    pub fn max_chars(mut self, max: usize) -> Self {
33        self.max_chars = Some(max);
34        self
35    }
36
37    /// Require the string to match `pattern`.
38    ///
39    /// Returns `Err` with the compiler message if `pattern` is not a valid regular
40    /// expression, so the caller must `?` or unwrap it.
41    #[cfg(feature = "regex")]
42    pub fn regex(mut self, pattern: impl Into<String>) -> Result<Self, String> {
43        let pattern = pattern.into();
44        match regex::Regex::new(&pattern) {
45            Ok(compiled) => {
46                self.pattern = Some(compiled);
47                Ok(self)
48            }
49            Err(error) => Err(error.to_string()),
50        }
51    }
52}
53
54crate::impl_meta_methods!(Str);
55
56impl Validator for Str {
57    fn meta(&self) -> &Meta {
58        &self.meta
59    }
60
61    fn meta_mut(&mut self) -> &mut Meta {
62        &mut self.meta
63    }
64
65    fn check(&self, value: &mut Value) -> Result<(), Error> {
66        let text = match value {
67            Value::String(text) => text,
68            other => {
69                return Err(Error::new(ErrorKind::Type {
70                    expected: ValueType::String,
71                    found: other.type_name(),
72                }));
73            }
74        };
75
76        let length = text.chars().count();
77        if let Some(min) = self.min_chars
78            && length < min
79        {
80            return Err(Error::new(ErrorKind::TooShort { len: length, min }));
81        }
82        if let Some(max) = self.max_chars
83            && length > max
84        {
85            return Err(Error::new(ErrorKind::TooLong { len: length, max }));
86        }
87
88        #[cfg(feature = "regex")]
89        if let Some(pattern) = &self.pattern
90            && !pattern.is_match(text)
91        {
92            return Err(Error::new(ErrorKind::PatternMismatch {
93                pattern: pattern.as_str().to_string(),
94            }));
95        }
96
97        Ok(())
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn accepts_string() {
107        let mut value = Value::String("hi".into());
108        assert!(Str::new().validate(&mut value).is_ok());
109    }
110
111    #[test]
112    fn rejects_non_string() {
113        let mut value = Value::Int(1);
114        let error = Str::new().validate(&mut value).unwrap_err();
115        assert!(matches!(error.kind, ErrorKind::Type { .. }));
116    }
117
118    #[test]
119    fn enforces_min_chars() {
120        let mut value = Value::String("".into());
121        let error = Str::new().min_chars(1).validate(&mut value).unwrap_err();
122        assert!(matches!(error.kind, ErrorKind::TooShort { .. }));
123    }
124
125    #[test]
126    fn enforces_max_chars() {
127        let mut value = Value::String("toolong".into());
128        let error = Str::new().max_chars(3).validate(&mut value).unwrap_err();
129        assert!(matches!(error.kind, ErrorKind::TooLong { .. }));
130    }
131
132    #[cfg(feature = "regex")]
133    #[test]
134    fn regex_matches_and_rejects() {
135        let validator = Str::new().regex("^[a-z]+$").unwrap();
136        let mut ok = Value::String("abc".into());
137        assert!(validator.validate(&mut ok).is_ok());
138        let mut bad = Value::String("abc1".into());
139        let error = validator.validate(&mut bad).unwrap_err();
140        assert!(matches!(error.kind, ErrorKind::PatternMismatch { .. }));
141    }
142}