Skip to main content

mx20022_validate/rules/
length.rs

1//! String length validation rules derived from XSD `minLength` / `maxLength` facets.
2
3use crate::error::{Severity, ValidationError};
4use crate::rules::Rule;
5
6/// Validates that a string meets a minimum length requirement.
7///
8/// # Examples
9///
10/// ```
11/// use mx20022_validate::rules::length::MinLengthRule;
12/// use mx20022_validate::rules::Rule;
13///
14/// let rule = MinLengthRule::new(1);
15/// assert!(rule.validate("a", "/path").is_empty());
16/// assert!(!rule.validate("", "/path").is_empty());
17/// ```
18pub struct MinLengthRule {
19    min: usize,
20}
21
22impl MinLengthRule {
23    /// Create a new minimum-length rule.
24    pub fn new(min: usize) -> Self {
25        Self { min }
26    }
27}
28
29impl Rule for MinLengthRule {
30    fn id(&self) -> &'static str {
31        "MIN_LENGTH"
32    }
33
34    fn validate(&self, value: &str, path: &str) -> Vec<ValidationError> {
35        // XSD minLength operates on the number of Unicode code points (characters), not bytes.
36        let len = value.chars().count();
37        if len < self.min {
38            vec![ValidationError::new(
39                path,
40                Severity::Error,
41                "MIN_LENGTH",
42                format!(
43                    "Value length {len} is less than minimum length {}",
44                    self.min
45                ),
46            )]
47        } else {
48            vec![]
49        }
50    }
51}
52
53/// Validates that a string does not exceed a maximum length.
54///
55/// # Examples
56///
57/// ```
58/// use mx20022_validate::rules::length::MaxLengthRule;
59/// use mx20022_validate::rules::Rule;
60///
61/// let rule = MaxLengthRule::new(35);
62/// assert!(rule.validate("short text", "/path").is_empty());
63/// assert!(!rule.validate(&"x".repeat(36), "/path").is_empty());
64/// ```
65pub struct MaxLengthRule {
66    max: usize,
67}
68
69impl MaxLengthRule {
70    /// Create a new maximum-length rule.
71    pub fn new(max: usize) -> Self {
72        Self { max }
73    }
74}
75
76impl Rule for MaxLengthRule {
77    fn id(&self) -> &'static str {
78        "MAX_LENGTH"
79    }
80
81    fn validate(&self, value: &str, path: &str) -> Vec<ValidationError> {
82        let len = value.chars().count();
83        if len > self.max {
84            vec![ValidationError::new(
85                path,
86                Severity::Error,
87                "MAX_LENGTH",
88                format!("Value length {len} exceeds maximum length {}", self.max),
89            )]
90        } else {
91            vec![]
92        }
93    }
94}
95
96/// Validates that a string's length is within an inclusive range `[min, max]`.
97///
98/// This is a convenience wrapper that combines [`MinLengthRule`] and [`MaxLengthRule`].
99///
100/// # Examples
101///
102/// ```
103/// use mx20022_validate::rules::length::LengthRangeRule;
104/// use mx20022_validate::rules::Rule;
105///
106/// let rule = LengthRangeRule::new(1, 35);
107/// assert!(rule.validate("hello", "/path").is_empty());
108/// assert!(!rule.validate("", "/path").is_empty());
109/// assert!(!rule.validate(&"x".repeat(36), "/path").is_empty());
110/// ```
111pub struct LengthRangeRule {
112    min: usize,
113    max: usize,
114}
115
116impl LengthRangeRule {
117    /// Create a new length range rule.
118    pub fn new(min: usize, max: usize) -> Self {
119        Self { min, max }
120    }
121}
122
123impl Rule for LengthRangeRule {
124    fn id(&self) -> &'static str {
125        "LENGTH_RANGE"
126    }
127
128    fn validate(&self, value: &str, path: &str) -> Vec<ValidationError> {
129        let len = value.chars().count();
130        let mut errors = vec![];
131        if len < self.min {
132            errors.push(ValidationError::new(
133                path,
134                Severity::Error,
135                "MIN_LENGTH",
136                format!(
137                    "Value length {len} is less than minimum length {}",
138                    self.min
139                ),
140            ));
141        }
142        if len > self.max {
143            errors.push(ValidationError::new(
144                path,
145                Severity::Error,
146                "MAX_LENGTH",
147                format!("Value length {len} exceeds maximum length {}", self.max),
148            ));
149        }
150        errors
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use crate::rules::Rule;
158
159    // --- MinLengthRule ---
160
161    #[test]
162    fn min_length_passes_when_equal() {
163        let rule = MinLengthRule::new(3);
164        assert!(rule.validate("abc", "/p").is_empty());
165    }
166
167    #[test]
168    fn min_length_passes_when_longer() {
169        let rule = MinLengthRule::new(3);
170        assert!(rule.validate("abcdef", "/p").is_empty());
171    }
172
173    #[test]
174    fn min_length_fails_when_shorter() {
175        let rule = MinLengthRule::new(3);
176        let errors = rule.validate("ab", "/p");
177        assert_eq!(errors.len(), 1);
178        assert_eq!(errors[0].rule_id, "MIN_LENGTH");
179    }
180
181    #[test]
182    fn min_length_zero_always_passes() {
183        let rule = MinLengthRule::new(0);
184        assert!(rule.validate("", "/p").is_empty());
185    }
186
187    // --- MaxLengthRule ---
188
189    #[test]
190    fn max_length_passes_when_equal() {
191        let rule = MaxLengthRule::new(35);
192        assert!(rule.validate(&"x".repeat(35), "/p").is_empty());
193    }
194
195    #[test]
196    fn max_length_passes_when_shorter() {
197        let rule = MaxLengthRule::new(35);
198        assert!(rule.validate("short", "/p").is_empty());
199    }
200
201    #[test]
202    fn max_length_fails_when_longer() {
203        let rule = MaxLengthRule::new(35);
204        let errors = rule.validate(&"x".repeat(36), "/p");
205        assert_eq!(errors.len(), 1);
206        assert_eq!(errors[0].rule_id, "MAX_LENGTH");
207    }
208
209    #[test]
210    fn max_length_empty_string_passes_when_max_positive() {
211        let rule = MaxLengthRule::new(5);
212        assert!(rule.validate("", "/p").is_empty());
213    }
214
215    // --- LengthRangeRule ---
216
217    #[test]
218    fn range_passes_within_bounds() {
219        let rule = LengthRangeRule::new(1, 35);
220        assert!(rule.validate("hello world", "/p").is_empty());
221    }
222
223    #[test]
224    fn range_fails_below_min() {
225        let rule = LengthRangeRule::new(1, 35);
226        let errors = rule.validate("", "/p");
227        assert_eq!(errors.len(), 1);
228        assert_eq!(errors[0].rule_id, "MIN_LENGTH");
229    }
230
231    #[test]
232    fn range_fails_above_max() {
233        let rule = LengthRangeRule::new(1, 5);
234        let errors = rule.validate("toolong", "/p");
235        assert_eq!(errors.len(), 1);
236        assert_eq!(errors[0].rule_id, "MAX_LENGTH");
237    }
238
239    #[test]
240    fn length_counts_unicode_code_points_not_bytes() {
241        // "é" is 2 UTF-8 bytes but 1 code point; max=1 should pass
242        let rule = MaxLengthRule::new(1);
243        assert!(rule.validate("é", "/p").is_empty());
244    }
245}