1use crate::error::{Error, FieldError, Result};
23
24#[derive(Debug, Default)]
26pub struct Validator {
27 errors: Vec<FieldError>,
28}
29
30impl Validator {
31 pub fn new() -> Self {
32 Self::default()
33 }
34
35 pub fn error(&mut self, field: &str, message: impl Into<String>) -> &mut Self {
37 self.errors.push(FieldError {
38 field: field.to_string(),
39 message: message.into(),
40 });
41 self
42 }
43
44 pub fn check(&mut self, condition: bool, field: &str, message: impl Into<String>) -> &mut Self {
46 if !condition {
47 self.error(field, message);
48 }
49 self
50 }
51
52 pub fn non_empty(&mut self, field: &str, value: &str) -> &mut Self {
54 self.check(!value.trim().is_empty(), field, "must not be empty")
55 }
56
57 pub fn len_between(&mut self, field: &str, value: &str, min: usize, max: usize) -> &mut Self {
59 let len = value.chars().count();
60 if len < min || len > max {
61 self.error(field, format!("must be {min} to {max} characters long"));
62 }
63 self
64 }
65
66 pub fn range<T: PartialOrd + std::fmt::Display>(
68 &mut self,
69 field: &str,
70 value: T,
71 min: T,
72 max: T,
73 ) -> &mut Self {
74 if value < min || value > max {
75 self.error(field, format!("must be between {min} and {max}"));
76 }
77 self
78 }
79
80 pub fn email(&mut self, field: &str, value: &str) -> &mut Self {
83 let ok = value.split_once('@').is_some_and(|(local, domain)| {
84 !local.is_empty()
85 && !domain.contains('@')
86 && domain.split('.').count() >= 2
87 && domain.split('.').all(|p| !p.is_empty())
88 }) && !value.chars().any(|c| c.is_whitespace() || c.is_control())
89 && value.len() <= 254;
90 self.check(ok, field, "must be a valid e-mail address")
91 }
92
93 pub fn is_valid(&self) -> bool {
94 self.errors.is_empty()
95 }
96
97 pub fn finish(self) -> Result<()> {
99 if self.errors.is_empty() {
100 Ok(())
101 } else {
102 Err(Error::Validation(self.errors))
103 }
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn collects_every_problem() {
113 let mut v = Validator::new();
114 v.non_empty("a", " ")
115 .len_between("b", "xy", 3, 5)
116 .range("c", 9, 0, 3);
117 v.error("d", "custom").check(true, "e", "never");
118 assert!(!v.is_valid());
119 let err = v.finish().unwrap_err();
120 let fields: Vec<_> = err.fields().iter().map(|f| f.field.as_str()).collect();
121 assert_eq!(fields, ["a", "b", "c", "d"]);
122 assert_eq!(err.status_code(), 422);
123 assert_eq!(err.message(), "Validation failed");
124 assert_eq!(err.fields()[1].message, "must be 3 to 5 characters long");
125 assert_eq!(err.fields()[2].message, "must be between 0 and 3");
126 }
127
128 #[test]
129 fn ok_when_nothing_failed() {
130 let mut v = Validator::new();
131 v.non_empty("a", "x")
132 .len_between("b", "äöü", 3, 3)
133 .range("c", 1.5, 0.0, 2.0);
134 assert!(v.is_valid());
135 assert!(v.finish().is_ok());
136 }
137
138 #[test]
139 fn email_shapes() {
140 let ok = |s: &str| {
141 let mut v = Validator::new();
142 v.email("email", s);
143 v.is_valid()
144 };
145 for good in ["a@b.co", "first.last+tag@sub.example.org"] {
146 assert!(ok(good), "{good}");
147 }
148 for bad in [
149 "",
150 "a",
151 "a@",
152 "@b.co",
153 "a@b",
154 "a@@b.co",
155 "a b@c.de",
156 "a@b..co",
157 "a@.co",
158 "a@b.co\n",
159 &format!("{}@b.co", "x".repeat(260)),
160 ] {
161 assert!(!ok(bad), "{bad:?}");
162 }
163 }
164}