1use serde::{Deserialize, Serialize};
33
34pub trait Validate {
59 fn validate(&self) -> Result<(), Vec<ValidationError>>;
65}
66
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, thiserror::Error)]
69#[error("{path}: {message}")]
70pub struct ValidationError {
71 pub path: String,
74 pub constraint: String,
77 pub message: String,
79}
80
81impl ValidationError {
82 pub fn new(
84 path: impl Into<String>,
85 constraint: impl Into<String>,
86 message: impl Into<String>,
87 ) -> Self {
88 Self {
89 path: path.into(),
90 constraint: constraint.into(),
91 message: message.into(),
92 }
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
103#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
104pub enum Constraint {
105 Min(f64),
107 Max(f64),
109 Length {
111 min: Option<u32>,
113 max: Option<u32>,
115 },
116 Pattern(String),
119 Email,
122 Url,
124 Custom(String),
127}
128
129pub mod check {
136 use super::ValidationError;
137
138 fn truncate_for_message(s: &str) -> String {
145 const LIMIT: usize = 20;
146 let mut iter = s.chars();
147 let head: String = iter.by_ref().take(LIMIT).collect();
148 if iter.next().is_some() {
149 format!("{head}…")
150 } else {
151 head
152 }
153 }
154
155 pub fn min<T>(path: &str, value: T, min: f64) -> Result<(), ValidationError>
167 where
168 T: Into<f64> + Copy,
169 {
170 let v: f64 = value.into();
171 if v < min {
172 Err(ValidationError::new(
173 path,
174 "min",
175 format!("value {v} is less than minimum {min}"),
176 ))
177 } else {
178 Ok(())
179 }
180 }
181
182 pub fn max<T>(path: &str, value: T, max: f64) -> Result<(), ValidationError>
190 where
191 T: Into<f64> + Copy,
192 {
193 let v: f64 = value.into();
194 if v > max {
195 Err(ValidationError::new(
196 path,
197 "max",
198 format!("value {v} is greater than maximum {max}"),
199 ))
200 } else {
201 Ok(())
202 }
203 }
204
205 pub fn length(
213 path: &str,
214 s: &str,
215 min: Option<u32>,
216 max: Option<u32>,
217 ) -> Result<(), ValidationError> {
218 let len = s.chars().count() as u64;
219 let out_of_range = match (min, max) {
220 (Some(lo), _) if len < u64::from(lo) => true,
221 (_, Some(hi)) if len > u64::from(hi) => true,
222 _ => false,
223 };
224 if !out_of_range {
225 return Ok(());
226 }
227 let lo = min.map_or_else(|| "no minimum".to_string(), |n| n.to_string());
228 let hi = max.map_or_else(|| "no maximum".to_string(), |n| n.to_string());
229 Err(ValidationError::new(
230 path,
231 "length",
232 format!("length {len} is outside [{lo}, {hi}]"),
233 ))
234 }
235
236 pub fn email(path: &str, s: &str) -> Result<(), ValidationError> {
244 let bad = || {
245 ValidationError::new(
246 path,
247 "email",
248 format!("not a valid email: {}", truncate_for_message(s)),
249 )
250 };
251 let at = s.find('@').ok_or_else(bad)?;
252 if at == 0 {
253 return Err(bad());
254 }
255 let after_at = &s[at + 1..];
256 let dot = after_at.find('.').ok_or_else(bad)?;
257 if dot == 0 || dot + 1 >= after_at.len() {
258 return Err(bad());
259 }
260 Ok(())
261 }
262
263 pub fn url(path: &str, s: &str) -> Result<(), ValidationError> {
268 if s.starts_with("http://") || s.starts_with("https://") {
269 Ok(())
270 } else {
271 Err(ValidationError::new(
272 path,
273 "url",
274 format!("not a valid url: {}", truncate_for_message(s)),
275 ))
276 }
277 }
278
279 pub fn pattern(path: &str, s: &str, regex_src: &str) -> Result<(), ValidationError> {
294 let re = match regex::Regex::new(regex_src) {
295 Ok(re) => re,
296 Err(e) => {
297 return Err(ValidationError::new(
298 path,
299 "pattern",
300 format!("invalid regex pattern: {e}"),
301 ));
302 }
303 };
304 if re.is_match(s) {
305 Ok(())
306 } else {
307 Err(ValidationError::new(
311 path,
312 "pattern",
313 format!("does not match pattern /{regex_src}/"),
314 ))
315 }
316 }
317}
318
319pub fn collect<F>(out: &mut Vec<ValidationError>, f: F)
337where
338 F: FnOnce() -> Result<(), ValidationError>,
339{
340 if let Err(e) = f() {
341 out.push(e);
342 }
343}
344
345pub fn run<F>(checks: F) -> Result<(), Vec<ValidationError>>
364where
365 F: FnOnce(&mut Vec<ValidationError>),
366{
367 let mut errors = Vec::new();
368 checks(&mut errors);
369 if errors.is_empty() {
370 Ok(())
371 } else {
372 Err(errors)
373 }
374}
375
376pub fn nested<V>(out: &mut Vec<ValidationError>, path_prefix: &str, value: &V)
412where
413 V: Validate + ?Sized,
414{
415 if let Err(inner) = value.validate() {
416 for mut e in inner {
417 e.path = if e.path.is_empty() {
418 path_prefix.to_string()
419 } else {
420 format!("{path_prefix}.{}", e.path)
421 };
422 out.push(e);
423 }
424 }
425}
426
427macro_rules! noop_validate {
443 ($($t:ty),* $(,)?) => {
444 $(
445 impl Validate for $t {
446 fn validate(&self) -> Result<(), Vec<ValidationError>> { Ok(()) }
447 }
448 )*
449 };
450}
451
452noop_validate!(
453 bool,
454 u8,
455 u16,
456 u32,
457 u64,
458 u128,
459 usize,
460 i8,
461 i16,
462 i32,
463 i64,
464 i128,
465 isize,
466 f32,
467 f64,
468 char,
469 String,
470 &'static str,
471 (),
472);
473
474impl<T: Validate> Validate for Option<T> {
475 fn validate(&self) -> Result<(), Vec<ValidationError>> {
476 match self {
477 Some(v) => v.validate(),
478 None => Ok(()),
479 }
480 }
481}
482
483impl<T: Validate> Validate for Vec<T> {
484 fn validate(&self) -> Result<(), Vec<ValidationError>> {
485 let mut errors = Vec::new();
486 for (i, v) in self.iter().enumerate() {
487 if let Err(mut errs) = v.validate() {
488 for e in &mut errs {
489 e.path = if e.path.is_empty() {
490 format!("[{i}]")
491 } else {
492 format!("[{i}].{}", e.path)
493 };
494 }
495 errors.append(&mut errs);
496 }
497 }
498 if errors.is_empty() {
499 Ok(())
500 } else {
501 Err(errors)
502 }
503 }
504}
505
506impl<T: Validate> Validate for Box<T> {
507 fn validate(&self) -> Result<(), Vec<ValidationError>> {
508 (**self).validate()
509 }
510}
511
512impl<K, V: Validate, S: std::hash::BuildHasher> Validate for std::collections::HashMap<K, V, S> {
513 fn validate(&self) -> Result<(), Vec<ValidationError>> {
514 let mut errors = Vec::new();
515 for v in self.values() {
516 if let Err(mut errs) = v.validate() {
517 errors.append(&mut errs);
518 }
519 }
520 if errors.is_empty() {
521 Ok(())
522 } else {
523 Err(errors)
524 }
525 }
526}
527
528macro_rules! tuple_validate {
531 ($($name:ident),+) => {
532 impl<$($name: Validate),+> Validate for ($($name,)+) {
533 #[allow(non_snake_case)]
534 fn validate(&self) -> Result<(), Vec<ValidationError>> {
535 let ($($name,)+) = self;
536 let mut errors = Vec::new();
537 $(
538 if let Err(mut errs) = $name.validate() { errors.append(&mut errs); }
539 )+
540 if errors.is_empty() { Ok(()) } else { Err(errors) }
541 }
542 }
543 };
544}
545tuple_validate!(A);
546tuple_validate!(A, B);
547tuple_validate!(A, B, C);
548tuple_validate!(A, B, C, D);
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553
554 #[test]
557 fn check_min_below_fails() {
558 let err = check::min("x", 0.5_f64, 1.0).expect_err("0.5 < 1.0 should fail");
559 assert_eq!(err.path, "x");
560 assert_eq!(err.constraint, "min");
561 }
562
563 #[test]
564 fn check_min_at_boundary_ok() {
565 check::min("x", 1.0_f64, 1.0).expect("value == min should pass");
566 }
567
568 #[test]
569 fn check_min_above_ok() {
570 check::min("x", 2.0_f64, 1.0).expect("value > min should pass");
571 }
572
573 #[test]
574 fn check_max_above_fails() {
575 let err = check::max("x", 1.5_f64, 1.0).expect_err("1.5 > 1.0 should fail");
576 assert_eq!(err.path, "x");
577 assert_eq!(err.constraint, "max");
578 }
579
580 #[test]
581 fn check_max_at_boundary_ok() {
582 check::max("x", 1.0_f64, 1.0).expect("value == max should pass");
583 }
584
585 #[test]
586 fn check_max_below_ok() {
587 check::max("x", 0.5_f64, 1.0).expect("value < max should pass");
588 }
589
590 #[test]
593 fn check_min_accepts_i32() {
594 let v: i32 = -3;
595 let err = check::min("age", v, 0.0).expect_err("-3 < 0 should fail");
596 assert_eq!(err.constraint, "min");
597 check::min("age", 0_i32, 0.0).expect("0 == 0 passes");
598 check::min("age", 5_i32, 0.0).expect("5 > 0 passes");
599 }
600
601 #[test]
602 fn check_min_accepts_u64() {
603 let v: u32 = 0;
607 let err = check::min("count", v, 1.0).expect_err("0 < 1 should fail");
608 assert_eq!(err.constraint, "min");
609 check::min("count", 1_u32, 1.0).expect("1 == 1 passes");
610 check::min("count", 100_u8, 1.0).expect("u8 100 > 1 passes");
611 }
612
613 #[test]
614 fn check_max_accepts_i32() {
615 let v: i32 = 200;
616 let err = check::max("age", v, 150.0).expect_err("200 > 150 should fail");
617 assert_eq!(err.constraint, "max");
618 check::max("age", 150_i32, 150.0).expect("150 == 150 passes");
619 check::max("age", -3_i32, 150.0).expect("-3 < 150 passes");
620 }
621
622 #[test]
623 fn check_max_accepts_u32() {
624 let v: u32 = 1000;
625 let err = check::max("count", v, 500.0).expect_err("1000 > 500 should fail");
626 assert_eq!(err.constraint, "max");
627 check::max("count", 0_u32, 500.0).expect("0 < 500 passes");
628 check::max("count", 5_u8, 10.0).expect("u8 5 < 10 passes");
629 }
630
631 #[test]
632 fn check_min_max_message_includes_value() {
633 let err = check::min("x", -2_i32, 0.0).expect_err("-2 < 0");
634 assert!(err.message.contains("-2"), "got {}", err.message);
635 let err = check::max("x", 999_u32, 10.0).expect_err("999 > 10");
636 assert!(err.message.contains("999"), "got {}", err.message);
637 }
638
639 fn assert_message_shape(message: &str) {
649 assert!(message.len() <= 80, "message > 80 chars: {message:?}");
650 assert!(
651 !message.ends_with('.'),
652 "message ends with a period: {message:?}"
653 );
654 let first = message.chars().next().expect("non-empty message");
655 assert!(
656 !first.is_uppercase(),
657 "message starts with uppercase: {message:?}"
658 );
659 }
660
661 #[test]
662 fn check_min_message_exact_text() {
663 let err = check::min("x", -2_i32, 0.0).expect_err("-2 < 0");
664 assert_eq!(err.message, "value -2 is less than minimum 0");
665 assert_message_shape(&err.message);
666 assert!(
668 !err.message.contains("x:"),
669 "path leaked into message: {}",
670 err.message
671 );
672 }
673
674 #[test]
675 fn check_max_message_exact_text() {
676 let err = check::max("y", 5_i32, 1.0).expect_err("5 > 1");
677 assert_eq!(err.message, "value 5 is greater than maximum 1");
678 assert_message_shape(&err.message);
679 }
680
681 #[test]
682 fn check_length_message_both_bounds() {
683 let err =
684 check::length("name", "hello!", Some(2), Some(5)).expect_err("len 6 outside [2, 5]");
685 assert_eq!(err.message, "length 6 is outside [2, 5]");
686 assert_message_shape(&err.message);
687 }
688
689 #[test]
690 fn check_length_message_no_min() {
691 let err = check::length("name", "hello!", None, Some(5)).expect_err("len 6 outside [-, 5]");
692 assert_eq!(err.message, "length 6 is outside [no minimum, 5]");
693 assert_message_shape(&err.message);
694 }
695
696 #[test]
697 fn check_length_message_no_max() {
698 let err = check::length("name", "", Some(1), None).expect_err("len 0 outside [1, -]");
699 assert_eq!(err.message, "length 0 is outside [1, no maximum]");
700 assert_message_shape(&err.message);
701 }
702
703 #[test]
704 fn check_email_message_exact_text() {
705 let err = check::email("e", "nope").expect_err("not an email");
706 assert_eq!(err.message, "not a valid email: nope");
707 assert_message_shape(&err.message);
708 }
709
710 #[test]
711 fn check_email_message_truncates_long_input() {
712 let long = "x".repeat(30) + "@nope";
714 let err = check::email("e", &long).expect_err("malformed");
715 let head: String = "x".repeat(20);
716 assert_eq!(err.message, format!("not a valid email: {head}…"));
717 assert_message_shape(&err.message);
718 }
719
720 #[test]
721 fn check_url_message_exact_text() {
722 let err = check::url("u", "ftp://x").expect_err("ftp not allowed");
723 assert_eq!(err.message, "not a valid url: ftp://x");
724 assert_message_shape(&err.message);
725 }
726
727 #[test]
728 fn check_url_message_truncates_long_input() {
729 let long = "g".repeat(40);
730 let err = check::url("u", &long).expect_err("not a url");
731 let head: String = "g".repeat(20);
732 assert_eq!(err.message, format!("not a valid url: {head}…"));
733 assert_message_shape(&err.message);
734 }
735
736 #[test]
737 fn check_pattern_message_exact_text() {
738 let err = check::pattern("x", "abc", r"^\d+$").expect_err("no digits");
739 assert_eq!(err.message, r"does not match pattern /^\d+$/");
740 assert_message_shape(&err.message);
741 assert!(!err.message.contains("x:"), "got {}", err.message);
743 }
744
745 #[test]
748 fn check_length_max_only_ok() {
749 check::length("name", "hi", None, Some(5)).expect("len 2 <= 5");
750 }
751
752 #[test]
753 fn check_length_max_only_fails() {
754 let err =
755 check::length("name", "hello!", None, Some(5)).expect_err("len 6 > 5 should fail");
756 assert_eq!(err.constraint, "length");
757 assert_eq!(err.path, "name");
758 }
759
760 #[test]
761 fn check_length_min_and_max_ok() {
762 check::length("name", "hey", Some(2), Some(5)).expect("2 <= 3 <= 5");
763 }
764
765 #[test]
766 fn check_length_below_min_fails() {
767 let err = check::length("name", "x", Some(2), Some(5)).expect_err("len 1 < 2 should fail");
768 assert_eq!(err.constraint, "length");
769 }
770
771 #[test]
772 fn check_length_empty_with_min_fails() {
773 let err = check::length("name", "", Some(1), None).expect_err("empty string fails min(1)");
774 assert_eq!(err.constraint, "length");
775 }
776
777 #[test]
778 fn check_length_empty_no_min_ok() {
779 check::length("name", "", None, Some(5)).expect("empty allowed when no min");
780 }
781
782 #[test]
783 fn check_length_empty_no_bounds_ok() {
784 check::length("name", "", None, None).expect("no bounds always passes");
785 }
786
787 #[test]
788 fn check_length_counts_chars_not_bytes() {
789 check::length("name", "é", Some(1), Some(1)).expect("counts chars, not bytes");
791 }
792
793 #[test]
796 fn check_email_accepts_simple() {
797 check::email("e", "a@b.co").expect("a@b.co is valid");
798 }
799
800 #[test]
801 fn check_email_rejects_no_dot() {
802 check::email("e", "a@b").expect_err("a@b has no dot after @");
803 }
804
805 #[test]
806 fn check_email_rejects_empty() {
807 check::email("e", "").expect_err("empty string is not an email");
808 }
809
810 #[test]
811 fn check_email_rejects_no_domain() {
812 check::email("e", "a.b@").expect_err("a.b@ has nothing after @");
813 }
814
815 #[test]
816 fn check_email_rejects_leading_at() {
817 check::email("e", "@b.co").expect_err("nothing before @");
818 }
819
820 #[test]
821 fn check_email_error_carries_constraint() {
822 let err = check::email("user.email", "nope").expect_err("nope is not an email");
823 assert_eq!(err.path, "user.email");
824 assert_eq!(err.constraint, "email");
825 }
826
827 #[test]
830 fn check_url_accepts_https() {
831 check::url("u", "https://x").expect("https://x is allowed");
832 }
833
834 #[test]
835 fn check_url_accepts_http() {
836 check::url("u", "http://x").expect("http://x is allowed");
837 }
838
839 #[test]
840 fn check_url_rejects_ftp() {
841 let err = check::url("u", "ftp://x").expect_err("ftp scheme not allowed");
842 assert_eq!(err.constraint, "url");
843 assert_eq!(err.path, "u");
844 }
845
846 #[test]
847 fn check_url_rejects_empty() {
848 check::url("u", "").expect_err("empty is not a URL");
849 }
850
851 #[test]
854 fn check_pattern_matches() {
855 check::pattern("x", "abc123", r"\d+").expect("contains digits");
856 }
857
858 #[test]
859 fn check_pattern_anchored_full_match() {
860 check::pattern("x", "12345", r"^\d+$").expect("all digits");
861 }
862
863 #[test]
864 fn check_pattern_does_not_match() {
865 let err = check::pattern("x", "abc", r"^\d+$").expect_err("no digits");
866 assert_eq!(err.constraint, "pattern");
867 assert_eq!(err.path, "x");
868 assert!(err.message.contains(r"^\d+$"), "got {}", err.message);
869 }
870
871 #[test]
872 fn check_pattern_invalid_regex_returns_error_not_panic() {
873 let err = check::pattern("x", "abc", r"[unclosed")
875 .expect_err("invalid regex source must surface as ValidationError");
876 assert_eq!(err.constraint, "pattern");
877 assert_eq!(err.path, "x");
878 assert!(
879 err.message.starts_with("invalid regex pattern:"),
880 "got {}",
881 err.message
882 );
883 }
884
885 #[test]
886 fn check_pattern_empty_input_against_optional_pattern() {
887 check::pattern("x", "", r"^$").expect("empty matches ^$");
889 check::pattern("x", "x", r"^$").expect_err("non-empty does not match ^$");
890 }
891
892 #[test]
895 fn collect_pushes_on_err() {
896 let mut errors = Vec::new();
897 collect(&mut errors, || check::min("x", 0_i32, 1.0));
898 assert_eq!(errors.len(), 1);
899 assert_eq!(errors[0].constraint, "min");
900 assert_eq!(errors[0].path, "x");
901 }
902
903 #[test]
904 fn collect_skips_on_ok() {
905 let mut errors = Vec::new();
906 collect(&mut errors, || check::min("x", 5_i32, 1.0));
907 assert!(errors.is_empty());
908 }
909
910 #[test]
911 fn collect_accumulates_multiple_failures() {
912 let mut errors = Vec::new();
913 collect(&mut errors, || check::min("x", 0_i32, 1.0));
914 collect(&mut errors, || check::max("x", 100_i32, 10.0));
915 collect(&mut errors, || check::min("y", 5_i32, 1.0)); collect(&mut errors, || check::email("e", "nope"));
917 assert_eq!(errors.len(), 3);
918 assert_eq!(errors[0].constraint, "min");
919 assert_eq!(errors[1].constraint, "max");
920 assert_eq!(errors[2].constraint, "email");
921 }
922
923 #[test]
926 fn run_returns_ok_when_no_errors_pushed() {
927 let result = run(|_errors| {});
928 assert!(result.is_ok());
929 }
930
931 #[test]
932 fn run_returns_ok_when_all_checks_pass() {
933 let result = run(|errors| {
934 collect(errors, || check::min("x", 5_i32, 1.0));
935 collect(errors, || check::max("x", 5_i32, 10.0));
936 });
937 assert!(result.is_ok());
938 }
939
940 #[test]
941 fn run_returns_err_with_all_collected_failures() {
942 let result = run(|errors| {
943 collect(errors, || check::min("x", 0_i32, 1.0));
944 collect(errors, || check::max("y", 100_i32, 10.0));
945 });
946 let errs = result.expect_err("two failures should make Err");
947 assert_eq!(errs.len(), 2);
948 assert_eq!(errs[0].path, "x");
949 assert_eq!(errs[0].constraint, "min");
950 assert_eq!(errs[1].path, "y");
951 assert_eq!(errs[1].constraint, "max");
952 }
953
954 #[test]
955 fn run_does_not_short_circuit() {
956 let mut counter = 0;
959 let result = run(|errors| {
960 counter += 1;
961 collect(errors, || check::min("x", 0_i32, 1.0));
962 counter += 1;
963 collect(errors, || check::max("x", 100_i32, 10.0));
964 counter += 1;
965 });
966 assert_eq!(counter, 3);
967 assert_eq!(result.expect_err("two failures").len(), 2);
968 }
969
970 struct Inner {
973 a: i32,
974 }
975 impl Validate for Inner {
976 fn validate(&self) -> Result<(), Vec<ValidationError>> {
977 run(|errors| {
978 collect(errors, || check::min("a", self.a, 0.0));
979 })
980 }
981 }
982
983 struct Outer {
984 inner: Inner,
985 }
986 impl Validate for Outer {
987 fn validate(&self) -> Result<(), Vec<ValidationError>> {
988 run(|errors| {
989 nested(errors, "inner", &self.inner);
990 })
991 }
992 }
993
994 #[test]
995 fn nested_prefixes_inner_path() {
996 let outer = Outer {
997 inner: Inner { a: -1 },
998 };
999 let errs = outer.validate().expect_err("a < 0 should fail");
1000 assert_eq!(errs.len(), 1);
1001 assert_eq!(errs[0].path, "inner.a");
1002 assert_eq!(errs[0].constraint, "min");
1003 }
1004
1005 #[test]
1006 fn nested_passes_through_when_inner_ok() {
1007 let outer = Outer {
1008 inner: Inner { a: 5 },
1009 };
1010 outer.validate().expect("inner is valid");
1011 }
1012
1013 struct RootError;
1014 impl Validate for RootError {
1015 fn validate(&self) -> Result<(), Vec<ValidationError>> {
1016 Err(vec![ValidationError::new("", "custom", "root-level fail")])
1017 }
1018 }
1019
1020 #[test]
1021 fn nested_uses_prefix_alone_when_inner_path_empty() {
1022 let mut errors = Vec::new();
1023 nested(&mut errors, "field", &RootError);
1024 assert_eq!(errors.len(), 1);
1025 assert_eq!(errors[0].path, "field");
1026 assert_eq!(errors[0].constraint, "custom");
1027 }
1028
1029 #[test]
1030 fn nested_collects_multiple_inner_errors() {
1031 struct Multi;
1032 impl Validate for Multi {
1033 fn validate(&self) -> Result<(), Vec<ValidationError>> {
1034 run(|errors| {
1035 collect(errors, || check::min("a", 0_i32, 1.0));
1036 collect(errors, || check::max("b", 100_i32, 10.0));
1037 })
1038 }
1039 }
1040 let mut errors = Vec::new();
1041 nested(&mut errors, "wrap", &Multi);
1042 assert_eq!(errors.len(), 2);
1043 assert_eq!(errors[0].path, "wrap.a");
1044 assert_eq!(errors[1].path, "wrap.b");
1045 }
1046
1047 #[test]
1048 fn nested_pushes_nothing_when_inner_ok() {
1049 struct AlwaysOk;
1050 impl Validate for AlwaysOk {
1051 fn validate(&self) -> Result<(), Vec<ValidationError>> {
1052 Ok(())
1053 }
1054 }
1055 let mut errors = Vec::new();
1056 nested(&mut errors, "x", &AlwaysOk);
1057 assert!(errors.is_empty());
1058 }
1059
1060 #[test]
1061 fn nested_double_nesting_dotted_path() {
1062 struct Deeper {
1064 outer: Outer,
1065 }
1066 impl Validate for Deeper {
1067 fn validate(&self) -> Result<(), Vec<ValidationError>> {
1068 run(|errors| {
1069 nested(errors, "outer", &self.outer);
1070 })
1071 }
1072 }
1073 let d = Deeper {
1074 outer: Outer {
1075 inner: Inner { a: -1 },
1076 },
1077 };
1078 let errs = d.validate().expect_err("inner a < 0");
1079 assert_eq!(errs.len(), 1);
1080 assert_eq!(errs[0].path, "outer.inner.a");
1081 }
1082
1083 fn roundtrip(c: &Constraint) -> Constraint {
1086 let json = serde_json::to_string(c).expect("serialize Constraint");
1087 serde_json::from_str(&json).expect("deserialize Constraint")
1088 }
1089
1090 #[test]
1091 fn constraint_min_roundtrip() {
1092 let c = Constraint::Min(1.5);
1093 assert_eq!(roundtrip(&c), c);
1094 let json = serde_json::to_string(&c).expect("serialize");
1095 assert!(json.contains("\"kind\":\"min\""), "got {json}");
1096 }
1097
1098 #[test]
1099 fn constraint_max_roundtrip() {
1100 let c = Constraint::Max(10.0);
1101 assert_eq!(roundtrip(&c), c);
1102 let json = serde_json::to_string(&c).expect("serialize");
1103 assert!(json.contains("\"kind\":\"max\""), "got {json}");
1104 }
1105
1106 #[test]
1107 fn constraint_length_roundtrip() {
1108 let c = Constraint::Length {
1109 min: Some(1),
1110 max: Some(64),
1111 };
1112 assert_eq!(roundtrip(&c), c);
1113 let json = serde_json::to_string(&c).expect("serialize");
1114 assert!(json.contains("\"kind\":\"length\""), "got {json}");
1115 }
1116
1117 #[test]
1118 fn constraint_length_max_only_roundtrip() {
1119 let c = Constraint::Length {
1120 min: None,
1121 max: Some(64),
1122 };
1123 assert_eq!(roundtrip(&c), c);
1124 }
1125
1126 #[test]
1127 fn constraint_pattern_roundtrip() {
1128 let c = Constraint::Pattern(r"^\d+$".to_string());
1129 assert_eq!(roundtrip(&c), c);
1130 let json = serde_json::to_string(&c).expect("serialize");
1131 assert!(json.contains("\"kind\":\"pattern\""), "got {json}");
1132 }
1133
1134 #[test]
1135 fn constraint_email_roundtrip() {
1136 let c = Constraint::Email;
1137 assert_eq!(roundtrip(&c), c);
1138 let json = serde_json::to_string(&c).expect("serialize");
1139 assert!(json.contains("\"kind\":\"email\""), "got {json}");
1140 }
1141
1142 #[test]
1143 fn constraint_url_roundtrip() {
1144 let c = Constraint::Url;
1145 assert_eq!(roundtrip(&c), c);
1146 let json = serde_json::to_string(&c).expect("serialize");
1147 assert!(json.contains("\"kind\":\"url\""), "got {json}");
1148 }
1149
1150 #[test]
1151 fn constraint_custom_roundtrip() {
1152 let c = Constraint::Custom("must_be_prime".to_string());
1153 assert_eq!(roundtrip(&c), c);
1154 let json = serde_json::to_string(&c).expect("serialize");
1155 assert!(json.contains("\"kind\":\"custom\""), "got {json}");
1156 }
1157
1158 #[test]
1161 fn validation_error_display_uses_path_and_message() {
1162 let err = ValidationError::new("user.email", "email", "bad");
1163 assert_eq!(format!("{err}"), "user.email: bad");
1164 }
1165
1166 #[test]
1167 fn validation_error_serde_roundtrip() {
1168 let err = ValidationError::new("a.b", "min", "too small");
1169 let json = serde_json::to_string(&err).expect("serialize");
1170 let back: ValidationError = serde_json::from_str(&json).expect("deserialize");
1171 assert_eq!(back, err);
1172 }
1173
1174 #[test]
1177 fn validate_for_unit_returns_ok() {
1178 ().validate().expect("unit always validates");
1179 }
1180
1181 #[test]
1182 fn validate_for_primitives_all_return_ok() {
1183 true.validate().expect("bool always ok");
1184 42_u32.validate().expect("u32 always ok");
1185 "hello".to_string().validate().expect("String always ok");
1186 }
1187
1188 #[test]
1189 fn validate_for_option_t_calls_inner_when_some() {
1190 struct Field(i32);
1192 impl Validate for Field {
1193 fn validate(&self) -> Result<(), Vec<ValidationError>> {
1194 run(|errors| {
1195 collect(errors, || check::min("v", self.0, 0.0));
1196 })
1197 }
1198 }
1199
1200 let none: Option<Field> = None;
1202 none.validate().expect("None passes");
1203
1204 Some(Field(5))
1206 .validate()
1207 .expect("Some with valid inner passes");
1208
1209 let errs = Some(Field(-1))
1211 .validate()
1212 .expect_err("Some with -1 should fail inner check");
1213 assert_eq!(errs.len(), 1);
1214 assert_eq!(errs[0].constraint, "min");
1215 assert_eq!(errs[0].path, "v");
1216 }
1217
1218 #[test]
1219 fn validate_for_vec_indexes_path() {
1220 struct Field(i32);
1222 impl Validate for Field {
1223 fn validate(&self) -> Result<(), Vec<ValidationError>> {
1224 run(|errors| {
1225 collect(errors, || check::min("v", self.0, 0.0));
1226 })
1227 }
1228 }
1229
1230 struct RootFail;
1233 impl Validate for RootFail {
1234 fn validate(&self) -> Result<(), Vec<ValidationError>> {
1235 Err(vec![ValidationError::new("", "custom", "boom")])
1236 }
1237 }
1238
1239 let v = vec![Field(5), Field(-1), Field(10), Field(-2)];
1240 let errs = v.validate().expect_err("indices 1 and 3 should fail");
1241 assert_eq!(errs.len(), 2);
1242 assert_eq!(errs[0].path, "[1].v");
1243 assert_eq!(errs[0].constraint, "min");
1244 assert_eq!(errs[1].path, "[3].v");
1245 assert_eq!(errs[1].constraint, "min");
1246
1247 let empty: Vec<Field> = Vec::new();
1249 empty.validate().expect("empty Vec passes");
1250
1251 let ok = vec![Field(0), Field(1), Field(2)];
1253 ok.validate().expect("all-valid Vec passes");
1254
1255 let v = vec![RootFail, RootFail];
1257 let errs = v.validate().expect_err("both fail at root");
1258 assert_eq!(errs.len(), 2);
1259 assert_eq!(errs[0].path, "[0]");
1260 assert_eq!(errs[1].path, "[1]");
1261 }
1262
1263 #[test]
1264 fn validate_for_tuple_runs_all_arms() {
1265 struct Field(i32);
1267 impl Validate for Field {
1268 fn validate(&self) -> Result<(), Vec<ValidationError>> {
1269 run(|errors| {
1270 collect(errors, || check::min("v", self.0, 0.0));
1271 })
1272 }
1273 }
1274
1275 let one = (Field(-1),);
1277 let errs = one.validate().expect_err("single-arm tuple fails");
1278 assert_eq!(errs.len(), 1);
1279
1280 let two = (Field(-1), Field(-2));
1282 let errs = two.validate().expect_err("both arms fail");
1283 assert_eq!(errs.len(), 2, "tuple must not short-circuit");
1284
1285 let three = (Field(-1), Field(0), Field(-3));
1287 let errs = three.validate().expect_err("two of three fail");
1288 assert_eq!(errs.len(), 2);
1289
1290 let four = (Field(0), Field(1), Field(2), Field(3));
1292 four.validate().expect("all-valid 4-tuple passes");
1293
1294 let mixed: (u32, String, Field) = (1, "x".into(), Field(5));
1296 mixed.validate().expect("primitives + ok user type pass");
1297 }
1298}