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