1use crate::check;
17use crate::errors::Errors;
18use crate::input::Input;
19use crate::messages::{self, Messages, SizeKind};
20use crate::rule::{IntoRules, Rule, Rules};
21use rustlavel_core::Json;
22use rustlavel_http::Request;
23use std::collections::BTreeMap;
24
25#[derive(Debug, Clone, Default)]
27pub struct Validator {
28 input: Input,
29 fields: Vec<(String, Rules)>,
30 messages: Messages,
31 wants_json: bool,
32 flash: Option<(std::sync::Arc<dyn rustlavel_http::Flash>, String)>,
35 old_input: Option<rustlavel_core::Json>,
36}
37
38impl Validator {
39 pub fn new(input: impl Into<Input>) -> Self {
41 Validator { input: input.into(), ..Validator::default() }
42 }
43
44 pub fn from_request(request: &mut Request) -> Self {
47 let wants_json = request.wants_json();
48
49 let old_input = (!wants_json).then(|| rustlavel_http::flash::old_input_of(request));
53 let flash = (!wants_json)
54 .then(|| {
55 request.flash().cloned().map(|store| (store, request.previous_url()))
56 })
57 .flatten();
58
59 Validator {
60 input: Input::from_request(request),
61 wants_json,
62 flash,
63 old_input,
64 ..Validator::default()
65 }
66 }
67
68 pub fn rule(mut self, field: impl Into<String>, rules: impl IntoRules) -> Self {
70 self.fields.push((field.into(), rules.into_rules()));
71 self
72 }
73
74 pub fn rules(mut self, specs: &[(&str, &str)]) -> Self {
76 for (field, spec) in specs {
77 self = self.rule(*field, *spec);
78 }
79 self
80 }
81
82 pub fn message(mut self, key: impl Into<String>, message: impl Into<String>) -> Self {
84 self.messages.set(key, message);
85 self
86 }
87
88 pub fn attribute(mut self, field: impl Into<String>, label: impl Into<String>) -> Self {
90 self.messages.set_attribute(field, label);
91 self
92 }
93
94 pub fn with_messages(mut self, messages: Messages) -> Self {
96 self.messages = messages;
97 self
98 }
99
100 pub fn with_json(mut self, wants_json: bool) -> Self {
103 self.wants_json = wants_json;
104 self
105 }
106
107 pub fn input(&self) -> &Input {
108 &self.input
109 }
110
111 pub fn errors(&self) -> Errors {
113 let mut errors = Errors::new().with_json(self.wants_json);
114 for (field, rules) in &self.fields {
115 self.check(field, rules, &mut errors);
116 }
117 errors
118 }
119
120 fn leave_behind(&self, errors: Errors) -> Errors {
127 let Some((flash, back)) = &self.flash else { return errors };
128
129 flash.flash(rustlavel_http::flash::ERRORS_KEY, errors.to_field_json());
130 if let Some(old) = &self.old_input {
131 flash.flash(rustlavel_http::flash::OLD_INPUT_KEY, old.clone());
132 }
133 errors.redirecting_to(Some(back.clone()))
134 }
135
136 pub fn passes(&self) -> bool {
137 self.errors().is_empty()
138 }
139
140 pub fn fails(&self) -> bool {
141 !self.passes()
142 }
143
144 pub fn validate(&self) -> Result<Validated, Errors> {
146 let errors = self.errors();
147 if !errors.is_empty() {
148 return Err(self.leave_behind(errors));
149 }
150 let mut fields = BTreeMap::new();
151 for (field, _) in &self.fields {
152 if let Some(value) = self.input.get(field) {
153 fields.insert(field.clone(), value.clone());
154 }
155 }
156 Ok(Validated { fields })
157 }
158
159 fn check(&self, field: &str, rules: &Rules, errors: &mut Errors) {
160 let required = rules.has("required");
161
162 let Some(value) = self.input.get(field) else {
163 if required {
164 self.fail(field, &Rule::Required, SizeKind::String, errors);
165 }
166 return;
167 };
168
169 if required && is_blank(value) {
170 self.fail(field, &Rule::Required, SizeKind::String, errors);
171 return;
172 }
173
174 if matches!(value, Json::String(text) if text.trim().is_empty()) {
178 return;
179 }
180
181 if value.is_null() && rules.has("nullable") {
184 return;
185 }
186
187 for rule in rules.rules() {
188 if matches!(rule, Rule::Required | Rule::Nullable) {
189 continue;
190 }
191 if !self.satisfied(field, rule, value, rules) {
192 self.fail(field, rule, size_kind(value, rules), errors);
193 }
194 }
195 }
196
197 fn satisfied(&self, field: &str, rule: &Rule, value: &Json, rules: &Rules) -> bool {
198 match rule {
199 Rule::Required | Rule::Nullable => true,
200 Rule::String => matches!(value, Json::String(_)),
201 Rule::Integer => as_number(value).is_some_and(|n| n.fract() == 0.0),
202 Rule::Numeric => as_number(value).is_some(),
203 Rule::Boolean => is_boolean(value),
204 Rule::Email => text(value).is_some_and(|t| check::is_email(&t)),
205 Rule::Url => text(value).is_some_and(|t| check::is_url(&t)),
206 Rule::Alpha => text(value).is_some_and(|t| check::is_alpha(&t)),
207 Rule::AlphaNum => text(value).is_some_and(|t| check::is_alpha_num(&t)),
208 Rule::AlphaDash => text(value).is_some_and(|t| check::is_alpha_dash(&t)),
209 Rule::Date => text(value).is_some_and(|t| check::is_date(&t)),
210 Rule::Uuid => text(value).is_some_and(|t| check::is_uuid(&t)),
211 Rule::Array => matches!(value, Json::Array(_)),
212 Rule::Min(bound) => measure(value, rules).is_some_and(|(size, _)| size >= *bound),
213 Rule::Max(bound) => measure(value, rules).is_some_and(|(size, _)| size <= *bound),
214 Rule::Between(low, high) => {
215 measure(value, rules).is_some_and(|(size, _)| size >= *low && size <= *high)
216 }
217 Rule::Size(exact) => measure(value, rules).is_some_and(|(size, _)| size == *exact),
218 Rule::In(allowed) => text(value).is_some_and(|t| allowed.contains(&t)),
219 Rule::NotIn(denied) => text(value).is_some_and(|t| !denied.contains(&t)),
220 Rule::StartsWith(prefixes) => {
221 text(value).is_some_and(|t| prefixes.iter().any(|p| t.starts_with(p)))
222 }
223 Rule::EndsWith(suffixes) => {
224 text(value).is_some_and(|t| suffixes.iter().any(|s| t.ends_with(s)))
225 }
226 Rule::Confirmed => self.compares(value, &format!("{field}_confirmation"), true),
227 Rule::Same(other) => self.compares(value, other, true),
228 Rule::Different(other) => self.compares(value, other, false),
229 }
230 }
231
232 fn compares(&self, value: &Json, other: &str, want_equal: bool) -> bool {
236 self.input.get(other).is_some_and(|found| equivalent(value, found) == want_equal)
237 }
238
239 fn fail(&self, field: &str, rule: &Rule, kind: SizeKind, errors: &mut Errors) {
240 let template = self.messages.template(field, rule, kind);
241 let mut values = vec![("attribute", self.messages.label(field))];
242 match rule {
243 Rule::Min(bound) => values.push(("min", messages::format_number(*bound))),
244 Rule::Max(bound) => values.push(("max", messages::format_number(*bound))),
245 Rule::Between(low, high) => {
246 values.push(("min", messages::format_number(*low)));
247 values.push(("max", messages::format_number(*high)));
248 }
249 Rule::Size(exact) => values.push(("size", messages::format_number(*exact))),
250 Rule::In(list) | Rule::NotIn(list) | Rule::StartsWith(list) | Rule::EndsWith(list) => {
251 values.push(("values", messages::format_values(list)));
252 }
253 Rule::Same(other) | Rule::Different(other) => {
254 values.push(("other", self.messages.label(other)));
255 }
256 _ => {}
257 }
258 errors.add(field, messages::interpolate(&template, &values));
259 }
260}
261
262#[derive(Debug, Clone, Default, PartialEq)]
264pub struct Validated {
265 fields: BTreeMap<String, Json>,
266}
267
268impl Validated {
269 pub fn get(&self, name: &str) -> Option<&Json> {
270 self.fields.get(name)
271 }
272
273 pub fn has(&self, name: &str) -> bool {
274 self.fields.contains_key(name)
275 }
276
277 pub fn string(&self, name: &str) -> Option<String> {
280 text(self.get(name)?)
281 }
282
283 pub fn integer(&self, name: &str) -> Option<i64> {
284 let number = as_number(self.get(name)?)?;
285 (number.fract() == 0.0).then_some(number as i64)
286 }
287
288 pub fn number(&self, name: &str) -> Option<f64> {
289 as_number(self.get(name)?)
290 }
291
292 pub fn boolean(&self, name: &str) -> Option<bool> {
294 match self.get(name)? {
295 Json::Bool(value) => Some(*value),
296 Json::Number(number) => Some(*number != 0.0),
297 Json::String(text) => match text.as_str() {
298 "1" | "true" => Some(true),
299 "0" | "false" => Some(false),
300 _ => None,
301 },
302 _ => None,
303 }
304 }
305
306 pub fn array(&self, name: &str) -> Option<&[Json]> {
307 self.get(name)?.as_array()
308 }
309
310 pub fn fields(&self) -> &BTreeMap<String, Json> {
311 &self.fields
312 }
313
314 pub fn len(&self) -> usize {
315 self.fields.len()
316 }
317
318 pub fn is_empty(&self) -> bool {
319 self.fields.is_empty()
320 }
321
322 pub fn into_json(self) -> Json {
324 Json::Object(self.fields)
325 }
326}
327
328impl From<Validated> for Json {
329 fn from(validated: Validated) -> Self {
330 validated.into_json()
331 }
332}
333
334fn is_blank(value: &Json) -> bool {
336 match value {
337 Json::Null => true,
338 Json::String(text) => text.trim().is_empty(),
339 Json::Array(items) => items.is_empty(),
340 _ => false,
341 }
342}
343
344fn text(value: &Json) -> Option<String> {
347 match value {
348 Json::String(found) => Some(found.clone()),
349 Json::Number(_) | Json::Bool(_) => Some(value.to_string()),
350 Json::Null | Json::Array(_) | Json::Object(_) => None,
351 }
352}
353
354fn as_number(value: &Json) -> Option<f64> {
359 match value {
360 Json::Number(number) => Some(*number).filter(|n| n.is_finite()),
361 Json::String(found) => found.trim().parse::<f64>().ok().filter(|n| n.is_finite()),
362 _ => None,
363 }
364}
365
366fn is_boolean(value: &Json) -> bool {
367 match value {
368 Json::Bool(_) => true,
369 Json::Number(number) => *number == 0.0 || *number == 1.0,
370 Json::String(found) => matches!(found.as_str(), "0" | "1" | "true" | "false"),
371 _ => false,
372 }
373}
374
375fn equivalent(left: &Json, right: &Json) -> bool {
380 match (text(left), text(right)) {
381 (Some(left), Some(right)) => left == right,
382 _ => left == right,
383 }
384}
385
386fn measure(value: &Json, rules: &Rules) -> Option<(f64, SizeKind)> {
392 if let Json::Array(items) = value {
393 return Some((items.len() as f64, SizeKind::Array));
394 }
395 if rules.has("integer") || rules.has("numeric") {
396 return as_number(value).map(|number| (number, SizeKind::Numeric));
397 }
398 match value {
399 Json::Number(number) => Some((*number, SizeKind::Numeric)),
400 Json::String(found) => Some((found.chars().count() as f64, SizeKind::String)),
401 _ => None,
402 }
403}
404
405fn size_kind(value: &Json, rules: &Rules) -> SizeKind {
407 if let Some((_, kind)) = measure(value, rules) {
408 return kind;
409 }
410 if rules.has("integer") || rules.has("numeric") { SizeKind::Numeric } else { SizeKind::String }
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416 use rustlavel_http::Method;
417
418 fn message_for(value: Json, spec: &str) -> Option<String> {
420 Validator::new(Input::new().with("field", value))
421 .rule("field", spec)
422 .errors()
423 .first("field")
424 .map(str::to_string)
425 }
426
427 fn passes(value: Json, spec: &str) -> bool {
428 message_for(value, spec).is_none()
429 }
430
431 #[test]
432 fn required_accepts_a_value_and_rejects_every_shape_of_emptiness() {
433 assert!(passes(Json::from("ada"), "required"));
434 assert!(passes(Json::from(0), "required"), "zero is a value");
435 assert!(passes(Json::from(false), "required"), "false is a value");
436
437 assert!(!passes(Json::Null, "required"));
438 assert!(!passes(Json::from(""), "required"));
439 assert!(!passes(Json::from(" "), "required"));
440 assert!(!passes(Json::Array(vec![]), "required"));
441
442 let missing = Validator::new(Input::new()).rule("field", "required").errors();
443 assert_eq!(missing.first("field"), Some("The field field is required."));
444 }
445
446 #[test]
447 fn an_absent_field_without_required_trips_nothing() {
448 let errors = Validator::new(Input::new()).rule("age", "integer|min:18").errors();
449 assert!(errors.is_empty());
450 }
451
452 #[test]
453 fn nullable_lets_an_explicit_null_through_where_a_bare_rule_would_not() {
454 assert!(passes(Json::Null, "nullable|integer"));
455 assert!(!passes(Json::Null, "integer"));
456 }
457
458 #[test]
459 fn nullable_and_required_together_still_demand_a_value() {
460 assert!(!passes(Json::Null, "required|nullable|string"));
461 }
462
463 #[test]
464 fn a_blank_string_is_treated_as_absent_by_every_rule_but_required() {
465 assert!(passes(Json::from(""), "email|min:5"));
466 assert!(!passes(Json::from(""), "required|email"));
467 }
468
469 #[test]
470 fn string_accepts_text_and_rejects_other_json_types() {
471 assert!(passes(Json::from("ada"), "string"));
472 assert!(!passes(Json::from(7), "string"));
473 assert!(!passes(Json::from(true), "string"));
474 assert_eq!(
475 message_for(Json::from(7), "string").unwrap(),
476 "The field field must be a string."
477 );
478 }
479
480 #[test]
481 fn integer_accepts_a_whole_number_however_it_was_encoded() {
482 assert!(passes(Json::from(18), "integer"));
483 assert!(passes(Json::from("18"), "integer"), "a form sends numbers as text");
484 assert!(passes(Json::from(-3), "integer"));
485
486 assert!(!passes(Json::from(1.5), "integer"));
487 assert!(!passes(Json::from("1.5"), "integer"));
488 assert!(!passes(Json::from("eighteen"), "integer"));
489 assert_eq!(
490 message_for(Json::from("x"), "integer").unwrap(),
491 "The field field must be an integer."
492 );
493 }
494
495 #[test]
496 fn numeric_accepts_fractions_but_not_words_or_infinities() {
497 assert!(passes(Json::from(1.5), "numeric"));
498 assert!(passes(Json::from("-2.75"), "numeric"));
499
500 assert!(!passes(Json::from("abc"), "numeric"));
501 assert!(!passes(Json::from("inf"), "numeric"), "`inf` parses in Rust but is not a number a user typed");
502 assert!(!passes(Json::from(true), "numeric"));
503 }
504
505 #[test]
506 fn boolean_accepts_the_forms_a_checkbox_arrives_in() {
507 for value in [Json::from(true), Json::from(false), Json::from(1), Json::from(0)] {
508 assert!(passes(value.clone(), "boolean"), "{value} should be boolean");
509 }
510 for value in ["1", "0", "true", "false"] {
511 assert!(passes(Json::from(value), "boolean"), "{value} should be boolean");
512 }
513
514 assert!(!passes(Json::from("yes"), "boolean"));
515 assert!(!passes(Json::from(2), "boolean"));
516 assert_eq!(
517 message_for(Json::from("yes"), "boolean").unwrap(),
518 "The field field must be true or false."
519 );
520 }
521
522 #[test]
523 fn email_and_url_delegate_to_the_format_checks() {
524 assert!(passes(Json::from("ada@example.com"), "email"));
525 assert!(!passes(Json::from("ada@example"), "email"));
526 assert_eq!(
527 message_for(Json::from("nope"), "email").unwrap(),
528 "The field field must be a valid email address."
529 );
530
531 assert!(passes(Json::from("https://example.com"), "url"));
532 assert!(!passes(Json::from("example.com"), "url"));
533 assert_eq!(
534 message_for(Json::from("example.com"), "url").unwrap(),
535 "The field field must be a valid URL."
536 );
537 }
538
539 #[test]
540 fn min_counts_characters_for_a_string() {
541 assert!(passes(Json::from("abc"), "min:3"));
542 assert!(!passes(Json::from("ab"), "min:3"));
543 assert_eq!(
544 message_for(Json::from("ab"), "min:3").unwrap(),
545 "The field field must be at least 3 characters."
546 );
547 }
548
549 #[test]
550 fn min_compares_values_when_the_field_is_a_number() {
551 assert!(passes(Json::from(18), "integer|min:18"));
552 assert!(passes(Json::from("18"), "integer|min:18"), "a form value is still a number");
553 assert!(!passes(Json::from(17), "integer|min:18"));
554 assert_eq!(
555 message_for(Json::from(17), "integer|min:18").unwrap(),
556 "The field field must be at least 18."
557 );
558 }
559
560 #[test]
561 fn min_counts_items_for_an_array() {
562 assert!(passes(Json::Array(vec![Json::from("a"), Json::from("b")]), "array|min:2"));
563 assert_eq!(
564 message_for(Json::Array(vec![Json::from("a")]), "array|min:2").unwrap(),
565 "The field field must have at least 2 items."
566 );
567 }
568
569 #[test]
570 fn max_mirrors_min_across_the_same_three_readings() {
571 assert!(passes(Json::from("abc"), "max:3"));
572 assert!(!passes(Json::from("abcd"), "max:3"));
573 assert_eq!(
574 message_for(Json::from("abcd"), "max:3").unwrap(),
575 "The field field must not be greater than 3 characters."
576 );
577
578 assert!(passes(Json::from(3), "integer|max:3"));
579 assert_eq!(
580 message_for(Json::from(4), "integer|max:3").unwrap(),
581 "The field field must not be greater than 3."
582 );
583
584 assert_eq!(
585 message_for(Json::Array(vec![Json::from(1), Json::from(2)]), "array|max:1").unwrap(),
586 "The field field must not have more than 1 items."
587 );
588 }
589
590 #[test]
591 fn between_is_inclusive_on_both_ends() {
592 assert!(passes(Json::from(1), "integer|between:1,10"));
593 assert!(passes(Json::from(10), "integer|between:1,10"));
594 assert!(!passes(Json::from(11), "integer|between:1,10"));
595 assert_eq!(
596 message_for(Json::from(0), "integer|between:1,10").unwrap(),
597 "The field field must be between 1 and 10."
598 );
599 assert_eq!(
600 message_for(Json::from("ab"), "between:3,5").unwrap(),
601 "The field field must be between 3 and 5 characters."
602 );
603 }
604
605 #[test]
606 fn size_demands_an_exact_length_value_or_count() {
607 assert!(passes(Json::from("abcdef"), "size:6"));
608 assert!(!passes(Json::from("abcde"), "size:6"));
609 assert_eq!(
610 message_for(Json::from("abcde"), "size:6").unwrap(),
611 "The field field must be 6 characters."
612 );
613 assert!(passes(Json::from(6), "integer|size:6"));
614 assert!(passes(Json::Array(vec![Json::from(1)]), "array|size:1"));
615 }
616
617 #[test]
618 fn in_and_not_in_match_against_the_listed_values() {
619 assert!(passes(Json::from("draft"), "in:draft,published"));
620 assert!(!passes(Json::from("deleted"), "in:draft,published"));
621 assert_eq!(
622 message_for(Json::from("deleted"), "in:draft,published").unwrap(),
623 "The selected field is invalid."
624 );
625
626 assert!(passes(Json::from("ada"), "not_in:admin,root"));
627 assert!(!passes(Json::from("root"), "not_in:admin,root"));
628 }
629
630 #[test]
631 fn alpha_families_reject_the_characters_they_exclude() {
632 assert!(passes(Json::from("Ada"), "alpha"));
633 assert!(!passes(Json::from("Ada2"), "alpha"));
634 assert_eq!(
635 message_for(Json::from("Ada2"), "alpha").unwrap(),
636 "The field field must only contain letters."
637 );
638
639 assert!(passes(Json::from("Ada2"), "alpha_num"));
640 assert!(!passes(Json::from("Ada-2"), "alpha_num"));
641
642 assert!(passes(Json::from("ada-2_x"), "alpha_dash"));
643 assert!(!passes(Json::from("ada 2"), "alpha_dash"));
644 }
645
646 #[test]
647 fn starts_with_and_ends_with_accept_any_of_their_options() {
648 assert!(passes(Json::from("https://x.dev"), "starts_with:http,https"));
649 assert!(!passes(Json::from("ftp://x.dev"), "starts_with:http,https"));
650 assert_eq!(
651 message_for(Json::from("ftp://x.dev"), "starts_with:http,https").unwrap(),
652 "The field field must start with one of the following: http, https."
653 );
654
655 assert!(passes(Json::from("a@b.dev"), "ends_with:.com,.dev"));
656 assert!(!passes(Json::from("a@b.net"), "ends_with:.com,.dev"));
657 assert_eq!(
658 message_for(Json::from("a@b.net"), "ends_with:.com,.dev").unwrap(),
659 "The field field must end with one of the following: .com, .dev."
660 );
661 }
662
663 #[test]
664 fn date_uuid_and_array_report_their_own_shapes() {
665 assert!(passes(Json::from("2024-02-29"), "date"));
666 assert!(!passes(Json::from("2023-02-29"), "date"));
667 assert_eq!(
668 message_for(Json::from("nope"), "date").unwrap(),
669 "The field field must be a valid date in the format YYYY-MM-DD."
670 );
671
672 assert!(passes(Json::from("9f8b2c1a-4d3e-4f5a-8b7c-1d2e3f4a5b6c"), "uuid"));
673 assert!(!passes(Json::from("not-a-uuid"), "uuid"));
674 assert_eq!(
675 message_for(Json::from("x"), "uuid").unwrap(),
676 "The field field must be a valid UUID."
677 );
678
679 assert!(passes(Json::Array(vec![Json::from(1)]), "array"));
680 assert!(!passes(Json::from("a,b"), "array"));
681 assert_eq!(
682 message_for(Json::from("a"), "array").unwrap(),
683 "The field field must be an array."
684 );
685 }
686
687 #[test]
688 fn confirmed_looks_for_the_matching_confirmation_field() {
689 let input = Input::new().with("password", "secret").with("password_confirmation", "secret");
690 assert!(Validator::new(input).rule("password", "confirmed").passes());
691
692 let wrong = Input::new().with("password", "secret").with("password_confirmation", "typo");
693 let errors = Validator::new(wrong).rule("password", "confirmed").errors();
694 assert_eq!(errors.first("password"), Some("The password field confirmation does not match."));
695
696 let absent = Input::new().with("password", "secret");
698 assert!(Validator::new(absent).rule("password", "confirmed").fails());
699 }
700
701 #[test]
702 fn same_and_different_compare_against_another_field() {
703 let input = Input::new().with("password", "secret").with("repeat", "secret");
704 assert!(Validator::new(input.clone()).rule("repeat", "same:password").passes());
705
706 let errors = Validator::new(input).rule("repeat", "different:password").errors();
707 assert_eq!(errors.first("repeat"), Some("The repeat field and password must be different."));
708
709 let differing = Input::new().with("username", "ada").with("password", "secret");
710 assert!(Validator::new(differing.clone()).rule("password", "different:username").passes());
711
712 let errors = Validator::new(differing).rule("password", "same:username").errors();
713 assert_eq!(errors.first("password"), Some("The password field must match username."));
714 }
715
716 #[test]
717 fn same_compares_a_json_number_with_the_text_a_form_would_resend() {
718 let input = Input::new().with("total", 42).with("confirm_total", "42");
719 assert!(Validator::new(input).rule("confirm_total", "same:total").passes());
720 }
721
722 #[test]
723 fn every_failing_rule_on_a_field_is_reported() {
724 let errors = Validator::new(Input::new().with("email", "nope"))
725 .rule("email", "email|min:20")
726 .errors();
727
728 assert_eq!(errors.get("email").len(), 2);
729 assert_eq!(errors.len(), 2);
730 }
731
732 #[test]
733 fn a_custom_message_replaces_the_default_for_one_field_and_rule() {
734 let errors = Validator::new(Input::new())
735 .rule("email", "required")
736 .rule("name", "required")
737 .message("email.required", "We cannot reach you without an email.")
738 .errors();
739
740 assert_eq!(errors.first("email"), Some("We cannot reach you without an email."));
741 assert_eq!(errors.first("name"), Some("The name field is required."));
742 }
743
744 #[test]
745 fn a_custom_message_keeps_its_placeholders() {
746 let errors = Validator::new(Input::new().with("age", 12))
747 .rule("age", "integer|min:18")
748 .message("min", "You must be :min or older to sign up (:attribute).")
749 .errors();
750
751 assert_eq!(errors.first("age"), Some("You must be 18 or older to sign up (age)."));
752 }
753
754 #[test]
755 fn an_attribute_override_reaches_the_rendered_message() {
756 let errors = Validator::new(Input::new())
757 .rule("dob", "required")
758 .attribute("dob", "date of birth")
759 .errors();
760
761 assert_eq!(errors.first("dob"), Some("The date of birth field is required."));
762 }
763
764 #[test]
765 fn a_snake_case_field_reads_as_words_without_any_configuration() {
766 let errors = Validator::new(Input::new()).rule("email_address", "required").errors();
767 assert_eq!(errors.first("email_address"), Some("The email address field is required."));
768 }
769
770 #[test]
771 fn validated_data_is_the_checked_subset_and_nothing_else() {
772 let input = Input::new()
773 .with("email", "ada@example.com")
774 .with("age", 36)
775 .with("is_admin", true);
776
777 let data = Validator::new(input)
778 .rules(&[("email", "required|email"), ("age", "integer")])
779 .validate()
780 .unwrap();
781
782 assert_eq!(data.len(), 2);
783 assert!(!data.has("is_admin"), "a field with no rules must not ride along");
784 assert_eq!(data.string("email").as_deref(), Some("ada@example.com"));
785 assert_eq!(data.integer("age"), Some(36));
786 }
787
788 #[test]
789 fn validated_accessors_read_the_forms_a_wire_value_arrives_in() {
790 let input = Input::new()
791 .with("age", "36")
792 .with("rate", "1.5")
793 .with("active", "true")
794 .with("tags", Json::Array(vec![Json::from("a")]));
795
796 let data = Validator::new(input)
797 .rules(&[("age", "integer"), ("rate", "numeric"), ("active", "boolean"), ("tags", "array")])
798 .validate()
799 .unwrap();
800
801 assert_eq!(data.integer("age"), Some(36));
802 assert_eq!(data.number("rate"), Some(1.5));
803 assert_eq!(data.boolean("active"), Some(true));
804 assert_eq!(data.array("tags").unwrap().len(), 1);
805 assert_eq!(data.string("age").as_deref(), Some("36"));
806 assert_eq!(data.integer("missing"), None);
807 }
808
809 #[test]
810 fn validated_data_converts_to_a_json_object() {
811 let data = Validator::new(Input::new().with("name", "ada"))
812 .rule("name", "required|string")
813 .validate()
814 .unwrap();
815
816 assert_eq!(Json::from(data).to_string(), r#"{"name":"ada"}"#);
817 }
818
819 #[test]
820 fn a_nullable_field_that_was_sent_as_null_is_still_returned() {
821 let data = Validator::new(Input::new().with("nickname", Json::Null))
822 .rule("nickname", "nullable|string")
823 .validate()
824 .unwrap();
825
826 assert!(data.has("nickname"));
827 assert!(data.get("nickname").unwrap().is_null());
828 assert_eq!(data.string("nickname"), None);
829 }
830
831 #[test]
832 fn the_builder_and_the_string_syntax_validate_identically() {
833 let input = Input::new().with("email", "nope").with("age", 12);
834 let specs = Validator::new(input.clone())
835 .rules(&[("email", "required|email"), ("age", "integer|min:18")])
836 .errors();
837 let built = Validator::new(input)
838 .rule("email", Rule::required().email())
839 .rule("age", Rule::integer().min(18))
840 .errors();
841
842 assert_eq!(specs, built);
843 }
844
845 #[test]
846 fn validating_a_request_reads_a_json_body() {
847 let mut request = Request::new(Method::Post, "/users")
848 .with_json(Json::object([("email", "ada@example.com".into()), ("age", 36.into())]));
849
850 let data = Validator::from_request(&mut request)
851 .rules(&[("email", "required|email"), ("age", "required|integer|min:18")])
852 .validate()
853 .unwrap();
854
855 assert_eq!(data.string("email").as_deref(), Some("ada@example.com"));
856 assert_eq!(data.integer("age"), Some(36));
857 }
858
859 #[test]
860 fn validating_a_request_reads_a_form_body() {
861 let mut request = Request::new(Method::Post, "/register").with_form(&[
862 ("email", "ada@example.com"),
863 ("password", "secret123"),
864 ("password_confirmation", "secret123"),
865 ]);
866
867 let data = Validator::from_request(&mut request)
868 .rules(&[("email", "required|email"), ("password", "required|min:8|confirmed")])
869 .validate()
870 .unwrap();
871
872 assert_eq!(data.len(), 2);
873 assert_eq!(data.string("password").as_deref(), Some("secret123"));
874 }
875
876 #[test]
877 fn a_failing_form_request_reports_every_field() {
878 let mut request = Request::new(Method::Post, "/register")
879 .with_form(&[("email", "nope"), ("password", "short"), ("password_confirmation", "other")]);
880
881 let errors = Validator::from_request(&mut request)
882 .rules(&[("email", "required|email"), ("password", "required|min:8|confirmed")])
883 .validate()
884 .unwrap_err();
885
886 assert_eq!(errors.first("email"), Some("The email field must be a valid email address."));
887 assert_eq!(errors.get("password").len(), 2);
888 assert!(!errors.wants_json(), "a form post is a browser, not an API client");
889 }
890
891 #[test]
892 fn a_json_request_is_remembered_as_wanting_a_json_failure() {
893 let mut request =
894 Request::new(Method::Post, "/api/users").with_json(Json::object([("email", "no".into())]));
895
896 let errors = Validator::from_request(&mut request).rule("email", "email").validate().unwrap_err();
897 assert!(errors.wants_json());
898 }
899
900 #[test]
901 fn query_string_values_validate_like_any_other_input() {
902 let mut request = Request::new(Method::Get, "/search?page=2&per_page=500");
903
904 let errors = Validator::from_request(&mut request)
905 .rules(&[("page", "integer|min:1"), ("per_page", "integer|max:100")])
906 .validate()
907 .unwrap_err();
908
909 assert!(!errors.has("page"));
910 assert_eq!(errors.first("per_page"), Some("The per page field must not be greater than 100."));
911 }
912}