Skip to main content

rustrails_model/validations/
length.rs

1use std::collections::HashMap;
2use std::ops::RangeInclusive;
3
4use serde_json::{Value, json};
5
6use super::{Validator, ValidatorOptions};
7use crate::errors::{ErrorType, Errors};
8
9/// Validates string-like length constraints for an attribute.
10#[derive(Debug, Clone, Default)]
11pub struct LengthValidator {
12    minimum: Option<usize>,
13    maximum: Option<usize>,
14    exact: Option<usize>,
15    message: Option<String>,
16    too_short: Option<String>,
17    too_long: Option<String>,
18    wrong_length: Option<String>,
19    pub(crate) options: ValidatorOptions,
20}
21
22impl LengthValidator {
23    /// Creates a new length validator with no constraints.
24    #[must_use]
25    pub fn new() -> Self {
26        Self::default()
27    }
28
29    crate::validations::impl_common_validator_methods!();
30
31    /// Requires the value length to be at least `minimum`.
32    #[must_use]
33    pub fn minimum(mut self, minimum: usize) -> Self {
34        self.minimum = Some(minimum);
35        self
36    }
37
38    /// Requires the value length to be at most `maximum`.
39    #[must_use]
40    pub fn maximum(mut self, maximum: usize) -> Self {
41        self.maximum = Some(maximum);
42        self
43    }
44
45    /// Requires the value length to equal `exact`.
46    #[must_use]
47    pub fn is(mut self, exact: usize) -> Self {
48        self.exact = Some(exact);
49        self
50    }
51
52    /// Requires the value length to fall within the inclusive range.
53    #[must_use]
54    pub fn in_range(mut self, range: RangeInclusive<usize>) -> Self {
55        self.minimum = Some(*range.start());
56        self.maximum = Some(*range.end());
57        self
58    }
59
60    /// Overrides the default error message for every length failure.
61    #[must_use]
62    pub fn message(mut self, message: impl Into<String>) -> Self {
63        self.message = Some(message.into());
64        self
65    }
66
67    /// Overrides the default too-short error message.
68    #[must_use]
69    pub fn too_short_message(mut self, message: impl Into<String>) -> Self {
70        self.too_short = Some(message.into());
71        self
72    }
73
74    /// Overrides the default too-long error message.
75    #[must_use]
76    pub fn too_long_message(mut self, message: impl Into<String>) -> Self {
77        self.too_long = Some(message.into());
78        self
79    }
80
81    /// Overrides the default wrong-length error message.
82    #[must_use]
83    pub fn wrong_length_message(mut self, message: impl Into<String>) -> Self {
84        self.wrong_length = Some(message.into());
85        self
86    }
87
88    fn value_length(value: Option<&Value>) -> usize {
89        match value {
90            None | Some(Value::Null) => 0,
91            Some(Value::String(text)) => text.chars().count(),
92            Some(Value::Array(values)) => values.len(),
93            Some(Value::Object(values)) => values.len(),
94            Some(Value::Bool(flag)) => flag.to_string().chars().count(),
95            Some(Value::Number(number)) => number.to_string().chars().count(),
96        }
97    }
98
99    fn build_message(template: &str, count: usize) -> String {
100        template.replace("%{count}", &count.to_string())
101    }
102
103    fn details(count: usize) -> HashMap<String, Value> {
104        HashMap::from([(String::from("count"), json!(count))])
105    }
106
107    fn too_short_error_message(&self, count: usize) -> String {
108        let template = self
109            .too_short
110            .as_deref()
111            .or(self.message.as_deref())
112            .unwrap_or("is too short (minimum is %{count} characters)");
113        Self::build_message(template, count)
114    }
115
116    fn too_long_error_message(&self, count: usize) -> String {
117        let template = self
118            .too_long
119            .as_deref()
120            .or(self.message.as_deref())
121            .unwrap_or("is too long (maximum is %{count} characters)");
122        Self::build_message(template, count)
123    }
124
125    fn wrong_length_error_message(&self, count: usize) -> String {
126        let template = self
127            .wrong_length
128            .as_deref()
129            .or(self.message.as_deref())
130            .unwrap_or("is the wrong length (should be %{count} characters)");
131        Self::build_message(template, count)
132    }
133}
134
135impl Validator for LengthValidator {
136    fn validate(&self, attribute: &str, value: Option<&Value>, errors: &mut Errors) {
137        let length = Self::value_length(value);
138
139        if let Some(exact) = self.exact
140            && length != exact
141        {
142            errors.add_with_details(
143                attribute,
144                ErrorType::WrongLength,
145                self.wrong_length_error_message(exact),
146                Self::details(exact),
147            );
148            return;
149        }
150
151        if let Some(minimum) = self.minimum
152            && length < minimum
153        {
154            errors.add_with_details(
155                attribute,
156                ErrorType::TooShort,
157                self.too_short_error_message(minimum),
158                Self::details(minimum),
159            );
160        }
161
162        if let Some(maximum) = self.maximum
163            && length > maximum
164        {
165            errors.add_with_details(
166                attribute,
167                ErrorType::TooLong,
168                self.too_long_error_message(maximum),
169                Self::details(maximum),
170            );
171        }
172    }
173
174    fn name(&self) -> &str {
175        "length"
176    }
177
178    fn options(&self) -> &ValidatorOptions {
179        &self.options
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use serde_json::json;
186
187    use super::LengthValidator;
188    use crate::{
189        errors::{ErrorType, Errors},
190        validations::{ValidationSet, Validator},
191    };
192
193    fn validate_length(validator: LengthValidator, value: Option<serde_json::Value>) -> Errors {
194        let mut errors = Errors::new();
195        validator.validate("field", value.as_ref(), &mut errors);
196        errors
197    }
198
199    #[test]
200    fn minimum_fails_for_short_string() {
201        let validator = LengthValidator::new().minimum(3);
202        let mut errors = Errors::new();
203
204        validator.validate("name", Some(&json!("Al")), &mut errors);
205
206        assert_eq!(errors.on("name")[0].error_type, ErrorType::TooShort);
207    }
208
209    #[test]
210    fn maximum_fails_for_long_string() {
211        let validator = LengthValidator::new().maximum(5);
212        let mut errors = Errors::new();
213
214        validator.validate("name", Some(&json!("Roberto")), &mut errors);
215
216        assert_eq!(errors.on("name")[0].error_type, ErrorType::TooLong);
217    }
218
219    #[test]
220    fn exact_length_fails_when_mismatched() {
221        let validator = LengthValidator::new().is(4);
222        let mut errors = Errors::new();
223
224        validator.validate("code", Some(&json!("abc")), &mut errors);
225
226        assert_eq!(errors.on("code")[0].error_type, ErrorType::WrongLength);
227    }
228
229    #[test]
230    fn inclusive_range_passes_when_inside_bounds() {
231        let validator = LengthValidator::new().in_range(3..=5);
232        let mut errors = Errors::new();
233
234        validator.validate("name", Some(&json!("Alice")), &mut errors);
235
236        assert!(errors.is_empty());
237    }
238
239    #[test]
240    fn nil_value_fails_minimum_but_not_maximum() {
241        let mut minimum_errors = Errors::new();
242        let mut maximum_errors = Errors::new();
243
244        LengthValidator::new()
245            .minimum(1)
246            .validate("name", None, &mut minimum_errors);
247        LengthValidator::new()
248            .maximum(5)
249            .validate("name", None, &mut maximum_errors);
250
251        assert_eq!(minimum_errors.on("name")[0].error_type, ErrorType::TooShort);
252        assert!(maximum_errors.is_empty());
253    }
254
255    #[test]
256    fn counts_unicode_scalar_values() {
257        let validator = LengthValidator::new().is(5);
258        let mut errors = Errors::new();
259
260        validator.validate("title", Some(&json!("一二345六")), &mut errors);
261
262        assert_eq!(
263            errors.on("title")[0].message,
264            "is the wrong length (should be 5 characters)"
265        );
266    }
267
268    #[test]
269    fn custom_messages_override_defaults() {
270        let validator = LengthValidator::new()
271            .minimum(5)
272            .too_short_message("need %{count}");
273        let mut errors = Errors::new();
274
275        validator.validate("password", Some(&json!("abc")), &mut errors);
276
277        assert_eq!(errors.on("password")[0].message, "need 5");
278    }
279
280    #[test]
281    fn uses_array_length_for_arrays() {
282        let validator = LengthValidator::new().is(2);
283        let mut errors = Errors::new();
284
285        validator.validate("tags", Some(&json!(["a"])), &mut errors);
286
287        assert_eq!(errors.on("tags")[0].error_type, ErrorType::WrongLength);
288    }
289
290    #[test]
291    fn minimum_passes_on_boundary() {
292        let errors = validate_length(LengthValidator::new().minimum(3), Some(json!("Cat")));
293
294        assert!(errors.is_empty());
295    }
296
297    #[test]
298    fn maximum_passes_on_boundary() {
299        let errors = validate_length(LengthValidator::new().maximum(3), Some(json!("Cat")));
300
301        assert!(errors.is_empty());
302    }
303
304    #[test]
305    fn exact_length_passes_when_equal() {
306        let errors = validate_length(LengthValidator::new().is(4), Some(json!("code")));
307
308        assert!(errors.is_empty());
309    }
310
311    #[test]
312    fn inclusive_range_passes_on_lower_boundary() {
313        let errors = validate_length(LengthValidator::new().in_range(2..=4), Some(json!("ab")));
314
315        assert!(errors.is_empty());
316    }
317
318    #[test]
319    fn inclusive_range_passes_on_upper_boundary() {
320        let errors = validate_length(LengthValidator::new().in_range(2..=4), Some(json!("abcd")));
321
322        assert!(errors.is_empty());
323    }
324
325    #[test]
326    fn wrong_length_message_override_is_used() {
327        let errors = validate_length(
328            LengthValidator::new()
329                .is(2)
330                .wrong_length_message("need %{count} chars"),
331            Some(json!("abc")),
332        );
333
334        assert_eq!(errors.on("field")[0].message, "need 2 chars");
335    }
336
337    #[test]
338    fn too_long_message_override_is_used() {
339        let errors = validate_length(
340            LengthValidator::new()
341                .maximum(2)
342                .too_long_message("max %{count}"),
343            Some(json!("abcd")),
344        );
345
346        assert_eq!(errors.on("field")[0].message, "max 2");
347    }
348
349    #[test]
350    fn generic_message_overrides_too_short_default() {
351        let errors = validate_length(
352            LengthValidator::new()
353                .minimum(5)
354                .message("minimum %{count}"),
355            Some(json!("abc")),
356        );
357
358        assert_eq!(errors.on("field")[0].message, "minimum 5");
359    }
360
361    #[test]
362    fn generic_message_overrides_too_long_default() {
363        let errors = validate_length(
364            LengthValidator::new()
365                .maximum(2)
366                .message("maximum %{count}"),
367            Some(json!("abcd")),
368        );
369
370        assert_eq!(errors.on("field")[0].message, "maximum 2");
371    }
372
373    #[test]
374    fn generic_message_overrides_wrong_length_default() {
375        let errors = validate_length(
376            LengthValidator::new().is(2).message("exactly %{count}"),
377            Some(json!("abcd")),
378        );
379
380        assert_eq!(errors.on("field")[0].message, "exactly 2");
381    }
382
383    #[test]
384    fn allow_nil_skips_missing_values_in_validation_set() {
385        let mut set = ValidationSet::new();
386        set.add("title", LengthValidator::new().minimum(1).allow_nil());
387        let mut errors = Errors::new();
388
389        let _ = set.validate(&|_| None, &mut errors);
390
391        assert!(errors.is_empty());
392    }
393
394    #[test]
395    fn allow_blank_skips_whitespace_strings_in_validation_set() {
396        let mut set = ValidationSet::new();
397        set.add("title", LengthValidator::new().minimum(2).allow_blank());
398        let mut errors = Errors::new();
399
400        let _ = set.validate(&|_| Some(json!("   ")), &mut errors);
401
402        assert!(errors.is_empty());
403    }
404
405    #[test]
406    fn empty_array_fails_minimum() {
407        let errors = validate_length(LengthValidator::new().minimum(1), Some(json!([])));
408
409        assert_eq!(errors.on("field")[0].error_type, ErrorType::TooShort);
410    }
411
412    #[test]
413    fn array_within_range_passes() {
414        let errors = validate_length(LengthValidator::new().in_range(1..=3), Some(json!([1, 2])));
415
416        assert!(errors.is_empty());
417    }
418
419    #[test]
420    fn object_length_counts_keys() {
421        let errors = validate_length(
422            LengthValidator::new().is(2),
423            Some(json!({ "first": 1, "second": 2, "third": 3 })),
424        );
425
426        assert_eq!(
427            errors.on("field")[0].message,
428            "is the wrong length (should be 2 characters)"
429        );
430    }
431
432    #[test]
433    fn bool_length_counts_rendered_characters() {
434        let errors = validate_length(LengthValidator::new().is(4), Some(json!(true)));
435
436        assert!(errors.is_empty());
437    }
438
439    #[test]
440    fn number_length_counts_rendered_digits() {
441        let errors = validate_length(LengthValidator::new().maximum(3), Some(json!(1234)));
442
443        assert_eq!(errors.on("field")[0].error_type, ErrorType::TooLong);
444    }
445
446    #[test]
447    fn exact_length_returns_before_minimum_and_maximum_checks() {
448        let errors = validate_length(
449            LengthValidator::new().is(2).minimum(5).maximum(1),
450            Some(json!("abcd")),
451        );
452
453        assert_eq!(errors.count(), 1);
454        assert_eq!(errors.on("field")[0].error_type, ErrorType::WrongLength);
455    }
456
457    #[test]
458    fn length_errors_include_count_details() {
459        let errors = validate_length(LengthValidator::new().minimum(3), Some(json!("a")));
460
461        assert_eq!(errors.details()[0].details.get("count"), Some(&json!(3)));
462    }
463
464    #[test]
465    fn full_message_humanizes_attribute_name() {
466        let mut errors = Errors::new();
467        LengthValidator::new().maximum(2).validate(
468            "line_items_count",
469            Some(&json!("abcd")),
470            &mut errors,
471        );
472
473        assert_eq!(
474            errors.full_messages(),
475            vec!["Line items count is too long (maximum is 2 characters)".to_string()],
476        );
477    }
478}