1#[derive(Debug, Clone, PartialEq, Eq)]
14#[allow(missing_docs)]
15pub enum ValidationError {
16 Empty,
18 TooShort { min: usize, actual: usize },
20 TooLong { max: usize, actual: usize },
22 BelowMin { min: String, actual: String },
24 AboveMax { max: String, actual: String },
26 InvalidPattern { pattern: String },
28 NotInSet { allowed: Vec<String> },
30 Custom(String),
32}
33
34impl std::fmt::Display for ValidationError {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 match self {
37 Self::Empty => write!(f, "Value cannot be empty"),
38 Self::TooShort { min, actual } => {
39 write!(
40 f,
41 "Value too short: minimum {min}, got {actual}"
42 )
43 }
44 Self::TooLong { max, actual } => {
45 write!(f, "Value too long: maximum {max}, got {actual}")
46 }
47 Self::BelowMin { min, actual } => {
48 write!(
49 f,
50 "Value below minimum: min {min}, got {actual}"
51 )
52 }
53 Self::AboveMax { max, actual } => {
54 write!(
55 f,
56 "Value above maximum: max {max}, got {actual}"
57 )
58 }
59 Self::InvalidPattern { pattern } => {
60 write!(f, "Value doesn't match pattern: {pattern}")
61 }
62 Self::NotInSet { allowed } => {
63 write!(f, "Value not in allowed set: {allowed:?}")
64 }
65 Self::Custom(msg) => write!(f, "{msg}"),
66 }
67 }
68}
69
70impl std::error::Error for ValidationError {}
71
72pub type ValidationResult<T> = Result<T, ValidationError>;
74
75#[derive(Debug, Default)]
81pub struct Validator {
82 errors: Vec<(String, ValidationError)>,
83}
84
85impl Validator {
86 #[must_use]
88 pub fn new() -> Self {
89 Self::default()
90 }
91
92 pub fn check<F>(&mut self, field: &str, validation: F) -> &mut Self
94 where
95 F: FnOnce() -> Result<(), ValidationError>,
96 {
97 if let Err(e) = validation() {
98 self.errors.push((field.to_string(), e));
99 }
100 self
101 }
102
103 #[must_use]
105 pub const fn is_valid(&self) -> bool {
106 self.errors.is_empty()
107 }
108
109 #[must_use]
111 pub fn errors(&self) -> &[(String, ValidationError)] {
112 &self.errors
113 }
114
115 pub fn finish(self) -> Result<(), Vec<(String, ValidationError)>> {
122 if self.errors.is_empty() {
123 Ok(())
124 } else {
125 Err(self.errors)
126 }
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn validation_error_display_covers_every_variant() {
136 assert_eq!(
137 ValidationError::Empty.to_string(),
138 "Value cannot be empty"
139 );
140 assert_eq!(
141 ValidationError::TooShort { min: 3, actual: 1 }.to_string(),
142 "Value too short: minimum 3, got 1"
143 );
144 assert_eq!(
145 ValidationError::TooLong { max: 5, actual: 9 }.to_string(),
146 "Value too long: maximum 5, got 9"
147 );
148 assert_eq!(
149 ValidationError::BelowMin {
150 min: "1".into(),
151 actual: "0".into()
152 }
153 .to_string(),
154 "Value below minimum: min 1, got 0"
155 );
156 assert_eq!(
157 ValidationError::AboveMax {
158 max: "10".into(),
159 actual: "11".into()
160 }
161 .to_string(),
162 "Value above maximum: max 10, got 11"
163 );
164 assert_eq!(
165 ValidationError::InvalidPattern {
166 pattern: "email".into()
167 }
168 .to_string(),
169 "Value doesn't match pattern: email"
170 );
171 assert!(ValidationError::NotInSet {
172 allowed: vec!["a".into(), "b".into()]
173 }
174 .to_string()
175 .starts_with("Value not in allowed set:"));
176 assert_eq!(
177 ValidationError::Custom("x".into()).to_string(),
178 "x"
179 );
180 }
181
182 #[test]
183 fn validation_error_implements_std_error() {
184 let err = ValidationError::Empty;
185 let _: &dyn std::error::Error = &err;
186 }
187
188 #[test]
189 fn validator_accumulates_every_failure() {
190 let mut v = Validator::new();
191 v.check("name", || Err(ValidationError::Empty));
192 v.check("pattern", || {
193 Err(ValidationError::InvalidPattern {
194 pattern: "email".into(),
195 })
196 });
197 assert!(!v.is_valid());
198 assert_eq!(v.errors().len(), 2);
199 let errs = v.finish().unwrap_err();
200 assert_eq!(errs.len(), 2);
201 assert_eq!(errs[0].0, "name");
202 assert_eq!(errs[1].0, "pattern");
203 }
204
205 #[test]
206 fn validator_finish_ok_when_no_checks_failed() {
207 let v = Validator::new();
208 assert!(v.finish().is_ok());
209 }
210
211 #[test]
212 fn validator_check_skips_recording_on_ok() {
213 let mut v = Validator::new();
214 v.check("field", || Ok(()));
215 assert!(v.is_valid());
216 assert!(v.errors().is_empty());
217 }
218}