rustrails_model/validations/
length.rs1use 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#[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 #[must_use]
25 pub fn new() -> Self {
26 Self::default()
27 }
28
29 crate::validations::impl_common_validator_methods!();
30
31 #[must_use]
33 pub fn minimum(mut self, minimum: usize) -> Self {
34 self.minimum = Some(minimum);
35 self
36 }
37
38 #[must_use]
40 pub fn maximum(mut self, maximum: usize) -> Self {
41 self.maximum = Some(maximum);
42 self
43 }
44
45 #[must_use]
47 pub fn is(mut self, exact: usize) -> Self {
48 self.exact = Some(exact);
49 self
50 }
51
52 #[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 #[must_use]
62 pub fn message(mut self, message: impl Into<String>) -> Self {
63 self.message = Some(message.into());
64 self
65 }
66
67 #[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 #[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 #[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}