skp_validator_rules/string/
length.rs

1//! Length validation rule.
2
3use skp_validator_core::{Rule, ValidationContext, ValidationErrors, ValidationError, ValidationResult};
4
5/// Mode for length calculation.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7pub enum LengthMode {
8    /// Count Unicode characters (default)
9    #[default]
10    Chars,
11    /// Count bytes
12    Bytes,
13    /// Count grapheme clusters (requires unicode-segmentation)
14    Graphemes,
15}
16
17/// String/collection length validation rule.
18///
19/// # Example
20///
21/// ```rust
22/// use skp_validator_rules::string::length::LengthRule;
23/// use skp_validator_core::{Rule, ValidationContext};
24///
25/// let rule = LengthRule::new().min(3).max(50);
26/// let ctx = ValidationContext::default();
27///
28/// assert!(rule.validate("hello", &ctx).is_ok());
29/// assert!(rule.validate("ab", &ctx).is_err()); // Too short
30/// ```
31#[derive(Debug, Clone)]
32pub struct LengthRule {
33    /// Minimum length (inclusive)
34    pub min: Option<usize>,
35    /// Maximum length (inclusive)
36    pub max: Option<usize>,
37    /// Exact length (if set, min/max are ignored)
38    pub equal: Option<usize>,
39    /// Length calculation mode
40    pub mode: LengthMode,
41    /// Custom error message
42    pub message: Option<String>,
43}
44
45impl LengthRule {
46    /// Create a new length rule.
47    pub fn new() -> Self {
48        Self {
49            min: None,
50            max: None,
51            equal: None,
52            mode: LengthMode::default(),
53            message: None,
54        }
55    }
56
57    /// Set minimum length.
58    pub fn min(mut self, min: usize) -> Self {
59        self.min = Some(min);
60        self
61    }
62
63    /// Set maximum length.
64    pub fn max(mut self, max: usize) -> Self {
65        self.max = Some(max);
66        self
67    }
68
69    /// Set exact length.
70    pub fn equal(mut self, len: usize) -> Self {
71        self.equal = Some(len);
72        self
73    }
74
75    /// Set length calculation mode.
76    pub fn mode(mut self, mode: LengthMode) -> Self {
77        self.mode = mode;
78        self
79    }
80
81    /// Set custom error message.
82    pub fn message(mut self, msg: impl Into<String>) -> Self {
83        self.message = Some(msg.into());
84        self
85    }
86
87    /// Calculate length based on mode.
88    fn calculate_length(&self, s: &str) -> usize {
89        match self.mode {
90            LengthMode::Chars => s.chars().count(),
91            LengthMode::Bytes => s.len(),
92            LengthMode::Graphemes => {
93                // Simple approximation without unicode-segmentation
94                // In production, use the unicode-segmentation crate
95                s.chars().count()
96            }
97        }
98    }
99}
100
101impl Default for LengthRule {
102    fn default() -> Self {
103        Self::new()
104    }
105}
106
107impl Rule<str> for LengthRule {
108    fn validate(&self, value: &str, _ctx: &ValidationContext) -> ValidationResult<()> {
109        let len = self.calculate_length(value);
110
111        // Check exact length first
112        if let Some(exact) = self.equal
113            && len != exact
114        {
115            let msg = self.message.clone().unwrap_or_else(|| {
116                format!("Must be exactly {} characters", exact)
117            });
118            return Err(ValidationErrors::from_iter([
119                ValidationError::root("length.equal", msg)
120                    .with_param("expected", exact as i64)
121                    .with_param("actual", len as i64)
122            ]));
123        }
124
125        // Check min length
126        if let Some(min) = self.min
127            && len < min
128        {
129            let msg = self.message.clone().unwrap_or_else(|| {
130                format!("Must be at least {} characters", min)
131            });
132            return Err(ValidationErrors::from_iter([
133                ValidationError::root("length.min", msg)
134                    .with_param("min", min as i64)
135                    .with_param("actual", len as i64)
136            ]));
137        }
138
139        // Check max length
140        if let Some(max) = self.max
141            && len > max
142        {
143            let msg = self.message.clone().unwrap_or_else(|| {
144                format!("Must be at most {} characters", max)
145            });
146            return Err(ValidationErrors::from_iter([
147                ValidationError::root("length.max", msg)
148                    .with_param("max", max as i64)
149                    .with_param("actual", len as i64)
150            ]));
151        }
152
153        Ok(())
154    }
155
156    fn name(&self) -> &'static str {
157        "length"
158    }
159
160    fn default_message(&self) -> String {
161        if let Some(exact) = self.equal {
162            format!("Must be exactly {} characters", exact)
163        } else {
164            match (self.min, self.max) {
165                (Some(min), Some(max)) => format!("Must be between {} and {} characters", min, max),
166                (Some(min), None) => format!("Must be at least {} characters", min),
167                (None, Some(max)) => format!("Must be at most {} characters", max),
168                (None, None) => "Invalid length".to_string(),
169            }
170        }
171    }
172}
173
174impl Rule<String> for LengthRule {
175    fn validate(&self, value: &String, ctx: &ValidationContext) -> ValidationResult<()> {
176        <Self as Rule<str>>::validate(self, value.as_str(), ctx)
177    }
178
179    fn name(&self) -> &'static str {
180        "length"
181    }
182
183    fn default_message(&self) -> String {
184        <Self as Rule<str>>::default_message(self)
185    }
186}
187
188// Implement for collections
189impl<T> Rule<Vec<T>> for LengthRule {
190    fn validate(&self, value: &Vec<T>, _ctx: &ValidationContext) -> ValidationResult<()> {
191        let len = value.len();
192
193        if let Some(exact) = self.equal
194            && len != exact
195        {
196            let msg = self.message.clone().unwrap_or_else(|| {
197                format!("Must have exactly {} items", exact)
198            });
199            return Err(ValidationErrors::from_iter([
200                ValidationError::root("length.equal", msg)
201                    .with_param("expected", exact as i64)
202                    .with_param("actual", len as i64)
203            ]));
204        }
205
206        if let Some(min) = self.min
207            && len < min
208        {
209            let msg = self.message.clone().unwrap_or_else(|| {
210                format!("Must have at least {} items", min)
211            });
212            return Err(ValidationErrors::from_iter([
213                ValidationError::root("length.min", msg)
214                    .with_param("min", min as i64)
215                    .with_param("actual", len as i64)
216            ]));
217        }
218
219        if let Some(max) = self.max
220            && len > max
221        {
222            let msg = self.message.clone().unwrap_or_else(|| {
223                format!("Must have at most {} items", max)
224            });
225            return Err(ValidationErrors::from_iter([
226                ValidationError::root("length.max", msg)
227                    .with_param("max", max as i64)
228                    .with_param("actual", len as i64)
229            ]));
230        }
231
232        Ok(())
233    }
234
235    fn name(&self) -> &'static str {
236        "length"
237    }
238
239    fn default_message(&self) -> String {
240        "Invalid length".to_string()
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn test_min_length() {
250        let rule = LengthRule::new().min(3);
251        let ctx = ValidationContext::default();
252
253        assert!(rule.validate("abc", &ctx).is_ok());
254        assert!(rule.validate("abcd", &ctx).is_ok());
255        assert!(rule.validate("ab", &ctx).is_err());
256    }
257
258    #[test]
259    fn test_max_length() {
260        let rule = LengthRule::new().max(5);
261        let ctx = ValidationContext::default();
262
263        assert!(rule.validate("abc", &ctx).is_ok());
264        assert!(rule.validate("abcde", &ctx).is_ok());
265        assert!(rule.validate("abcdef", &ctx).is_err());
266    }
267
268    #[test]
269    fn test_range() {
270        let rule = LengthRule::new().min(3).max(5);
271        let ctx = ValidationContext::default();
272
273        assert!(rule.validate("ab", &ctx).is_err());
274        assert!(rule.validate("abc", &ctx).is_ok());
275        assert!(rule.validate("abcde", &ctx).is_ok());
276        assert!(rule.validate("abcdef", &ctx).is_err());
277    }
278
279    #[test]
280    fn test_exact() {
281        let rule = LengthRule::new().equal(5);
282        let ctx = ValidationContext::default();
283
284        assert!(rule.validate("abcd", &ctx).is_err());
285        assert!(rule.validate("abcde", &ctx).is_ok());
286        assert!(rule.validate("abcdef", &ctx).is_err());
287    }
288
289    #[test]
290    fn test_vec_length() {
291        let rule = LengthRule::new().min(2).max(4);
292        let ctx = ValidationContext::default();
293
294        assert!(rule.validate(&vec![1], &ctx).is_err());
295        assert!(rule.validate(&vec![1, 2], &ctx).is_ok());
296        assert!(rule.validate(&vec![1, 2, 3, 4], &ctx).is_ok());
297        assert!(rule.validate(&vec![1, 2, 3, 4, 5], &ctx).is_err());
298    }
299}