Skip to main content

karbon_framework/validation/constraints/string/
length.rs

1use crate::validation::constraints::{Constraint, ConstraintResult, ConstraintViolation};
2
3/// Validates that a string length is within a given range.
4///
5/// Equivalent to Symfony's `Length` constraint.
6pub struct Length {
7    pub min: Option<usize>,
8    pub max: Option<usize>,
9    pub exact: Option<usize>,
10    pub min_message: String,
11    pub max_message: String,
12    pub exact_message: String,
13}
14
15impl Default for Length {
16    fn default() -> Self {
17        Self {
18            min: None,
19            max: None,
20            exact: None,
21            min_message: "This value is too short. It should have {{ limit }} characters or more."
22                .to_string(),
23            max_message: "This value is too long. It should have {{ limit }} characters or less."
24                .to_string(),
25            exact_message: "This value should have exactly {{ limit }} characters.".to_string(),
26        }
27    }
28}
29
30impl Length {
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    pub fn min(mut self, min: usize) -> Self {
36        self.min = Some(min);
37        self
38    }
39
40    pub fn max(mut self, max: usize) -> Self {
41        self.max = Some(max);
42        self
43    }
44
45    pub fn exact(mut self, exact: usize) -> Self {
46        self.exact = Some(exact);
47        self
48    }
49
50    pub fn with_min_message(mut self, message: impl Into<String>) -> Self {
51        self.min_message = message.into();
52        self
53    }
54
55    pub fn with_max_message(mut self, message: impl Into<String>) -> Self {
56        self.max_message = message.into();
57        self
58    }
59
60    pub fn with_exact_message(mut self, message: impl Into<String>) -> Self {
61        self.exact_message = message.into();
62        self
63    }
64
65    fn format_message(&self, template: &str, limit: usize) -> String {
66        template.replace("{{ limit }}", &limit.to_string())
67    }
68}
69
70impl Constraint for Length {
71    fn validate(&self, value: &str) -> ConstraintResult {
72        let len = value.chars().count();
73
74        if let Some(exact) = self.exact {
75            if len != exact {
76                return Err(ConstraintViolation::new(
77                    self.name(),
78                    self.format_message(&self.exact_message, exact),
79                    value,
80                ));
81            }
82            return Ok(());
83        }
84
85        if let Some(min) = self.min {
86            if len < min {
87                return Err(ConstraintViolation::new(
88                    self.name(),
89                    self.format_message(&self.min_message, min),
90                    value,
91                ));
92            }
93        }
94
95        if let Some(max) = self.max {
96            if len > max {
97                return Err(ConstraintViolation::new(
98                    self.name(),
99                    self.format_message(&self.max_message, max),
100                    value,
101                ));
102            }
103        }
104
105        Ok(())
106    }
107
108    fn name(&self) -> &'static str {
109        "Length"
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_min_length() {
119        let constraint = Length::new().min(3);
120        assert!(constraint.validate("abc").is_ok());
121        assert!(constraint.validate("abcd").is_ok());
122        assert!(constraint.validate("ab").is_err());
123        assert!(constraint.validate("").is_err());
124    }
125
126    #[test]
127    fn test_max_length() {
128        let constraint = Length::new().max(5);
129        assert!(constraint.validate("abc").is_ok());
130        assert!(constraint.validate("abcde").is_ok());
131        assert!(constraint.validate("abcdef").is_err());
132    }
133
134    #[test]
135    fn test_min_max_range() {
136        let constraint = Length::new().min(2).max(5);
137        assert!(constraint.validate("ab").is_ok());
138        assert!(constraint.validate("abcde").is_ok());
139        assert!(constraint.validate("a").is_err());
140        assert!(constraint.validate("abcdef").is_err());
141    }
142
143    #[test]
144    fn test_exact_length() {
145        let constraint = Length::new().exact(4);
146        assert!(constraint.validate("abcd").is_ok());
147        assert!(constraint.validate("abc").is_err());
148        assert!(constraint.validate("abcde").is_err());
149    }
150
151    #[test]
152    fn test_unicode_characters() {
153        let constraint = Length::new().min(2).max(5);
154        assert!(constraint.validate("café").is_ok()); // 4 chars
155        assert!(constraint.validate("é").is_err()); // 1 char
156    }
157
158    #[test]
159    fn test_error_message_formatting() {
160        let constraint = Length::new().min(3);
161        let err = constraint.validate("ab").unwrap_err();
162        assert!(err.message.contains("3"));
163    }
164}