1use std::net::IpAddr;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19#[allow(missing_docs)]
20pub enum ValidationError {
21 Empty,
23 TooShort { min: usize, actual: usize },
25 TooLong { max: usize, actual: usize },
27 BelowMin { min: String, actual: String },
29 AboveMax { max: String, actual: String },
31 InvalidPattern { pattern: String },
33 NotInSet { allowed: Vec<String> },
35 Custom(String),
37}
38
39impl std::fmt::Display for ValidationError {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 match self {
42 Self::Empty => write!(f, "Value cannot be empty"),
43 Self::TooShort { min, actual } => {
44 write!(f, "Value too short: minimum {min}, got {actual}")
45 }
46 Self::TooLong { max, actual } => {
47 write!(f, "Value too long: maximum {max}, got {actual}")
48 }
49 Self::BelowMin { min, actual } => {
50 write!(f, "Value below minimum: min {min}, got {actual}")
51 }
52 Self::AboveMax { max, actual } => {
53 write!(f, "Value above maximum: max {max}, got {actual}")
54 }
55 Self::InvalidPattern { pattern } => {
56 write!(f, "Value doesn't match pattern: {pattern}")
57 }
58 Self::NotInSet { allowed } => {
59 write!(f, "Value not in allowed set: {:?}", allowed)
60 }
61 Self::Custom(msg) => write!(f, "{msg}"),
62 }
63 }
64}
65
66impl std::error::Error for ValidationError {}
67
68pub type ValidationResult<T> = Result<T, ValidationError>;
70
71pub fn validate_not_empty(value: &str) -> ValidationResult<&str> {
73 if value.trim().is_empty() {
74 Err(ValidationError::Empty)
75 } else {
76 Ok(value)
77 }
78}
79
80pub fn validate_length(value: &str, min: usize, max: usize) -> ValidationResult<&str> {
82 let len = value.len();
83 if len < min {
84 Err(ValidationError::TooShort { min, actual: len })
85 } else if len > max {
86 Err(ValidationError::TooLong { max, actual: len })
87 } else {
88 Ok(value)
89 }
90}
91
92pub fn validate_range<T>(value: T, min: T, max: T) -> ValidationResult<T>
94where
95 T: PartialOrd + std::fmt::Display + Copy,
96{
97 if value < min {
98 Err(ValidationError::BelowMin {
99 min: min.to_string(),
100 actual: value.to_string(),
101 })
102 } else if value > max {
103 Err(ValidationError::AboveMax {
104 max: max.to_string(),
105 actual: value.to_string(),
106 })
107 } else {
108 Ok(value)
109 }
110}
111
112#[must_use]
116pub fn is_valid_email(email: &str) -> bool {
117 let email = email.trim();
118
119 let parts: Vec<&str> = email.split('@').collect();
121 if parts.len() != 2 {
122 return false;
123 }
124
125 let (local, domain) = (parts[0], parts[1]);
126
127 if local.is_empty() || local.len() > 64 {
129 return false;
130 }
131
132 if domain.is_empty() || domain.len() > 255 {
134 return false;
135 }
136
137 if !domain.contains('.') {
139 return false;
140 }
141
142 if email.contains("..") {
144 return false;
145 }
146
147 true
148}
149
150#[must_use]
152pub fn is_valid_url(url: &str) -> bool {
153 let url = url.trim();
154
155 if !url.starts_with("http://") && !url.starts_with("https://") {
157 return false;
158 }
159
160 let rest = url
162 .strip_prefix("https://")
163 .or_else(|| url.strip_prefix("http://"));
164 match rest {
165 Some(r) => !r.is_empty() && r.contains('.'),
166 None => false,
167 }
168}
169
170#[must_use]
172pub fn is_valid_ip(ip: &str) -> bool {
173 ip.trim().parse::<IpAddr>().is_ok()
174}
175
176#[must_use]
178pub fn is_valid_ipv4(ip: &str) -> bool {
179 ip.trim().parse::<std::net::Ipv4Addr>().is_ok()
180}
181
182#[must_use]
184pub fn is_valid_ipv6(ip: &str) -> bool {
185 ip.trim().parse::<std::net::Ipv6Addr>().is_ok()
186}
187
188#[must_use]
190pub fn is_alphanumeric(s: &str) -> bool {
191 !s.is_empty() && s.chars().all(|c| c.is_alphanumeric())
192}
193
194#[must_use]
196pub fn is_identifier(s: &str) -> bool {
197 if s.is_empty() {
198 return false;
199 }
200
201 let mut chars = s.chars();
202
203 match chars.next() {
205 Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
206 _ => return false,
207 }
208
209 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
211}
212
213#[must_use]
215pub fn is_valid_semver(version: &str) -> bool {
216 let version = version.trim().strip_prefix('v').unwrap_or(version);
217 let parts: Vec<&str> = version.split('.').collect();
218
219 if parts.len() != 3 {
220 return false;
221 }
222
223 parts.iter().all(|part| {
224 let clean = part.split('-').next().unwrap_or(part);
225 clean.parse::<u64>().is_ok()
226 })
227}
228
229pub fn validate_in_set<T>(value: &T, allowed: &[T]) -> ValidationResult<()>
231where
232 T: PartialEq + std::fmt::Display,
233{
234 if allowed.contains(value) {
235 Ok(())
236 } else {
237 Err(ValidationError::NotInSet {
238 allowed: allowed.iter().map(|v| v.to_string()).collect(),
239 })
240 }
241}
242
243#[derive(Debug, Default)]
245pub struct Validator {
246 errors: Vec<(String, ValidationError)>,
247}
248
249impl Validator {
250 #[must_use]
252 pub fn new() -> Self {
253 Self::default()
254 }
255
256 pub fn check<F>(&mut self, field: &str, validation: F) -> &mut Self
258 where
259 F: FnOnce() -> Result<(), ValidationError>,
260 {
261 if let Err(e) = validation() {
262 self.errors.push((field.to_string(), e));
263 }
264 self
265 }
266
267 #[must_use]
269 pub fn is_valid(&self) -> bool {
270 self.errors.is_empty()
271 }
272
273 #[must_use]
275 pub fn errors(&self) -> &[(String, ValidationError)] {
276 &self.errors
277 }
278
279 pub fn finish(self) -> Result<(), Vec<(String, ValidationError)>> {
281 if self.errors.is_empty() {
282 Ok(())
283 } else {
284 Err(self.errors)
285 }
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn test_validate_not_empty() {
295 assert!(validate_not_empty("hello").is_ok());
296 assert!(validate_not_empty("").is_err());
297 assert!(validate_not_empty(" ").is_err());
298 }
299
300 #[test]
301 fn test_validate_length() {
302 assert!(validate_length("hello", 1, 10).is_ok());
303 assert!(validate_length("hi", 5, 10).is_err());
304 assert!(validate_length("hello world!", 1, 5).is_err());
305 }
306
307 #[test]
308 fn test_validate_range() {
309 assert!(validate_range(5, 1, 10).is_ok());
310 assert!(validate_range(0, 1, 10).is_err());
311 assert!(validate_range(15, 1, 10).is_err());
312 }
313
314 #[test]
315 fn test_is_valid_email() {
316 assert!(is_valid_email("user@example.com"));
317 assert!(is_valid_email("user.name@example.co.uk"));
318 assert!(!is_valid_email("invalid"));
319 assert!(!is_valid_email("@example.com"));
320 assert!(!is_valid_email("user@"));
321 assert!(!is_valid_email("user@@example.com"));
322 }
323
324 #[test]
325 fn test_is_valid_url() {
326 assert!(is_valid_url("https://example.com"));
327 assert!(is_valid_url("http://example.com/path"));
328 assert!(!is_valid_url("example.com"));
329 assert!(!is_valid_url("ftp://example.com"));
330 assert!(!is_valid_url("https://"));
331 }
332
333 #[test]
334 fn test_is_valid_ip() {
335 assert!(is_valid_ip("192.168.1.1"));
336 assert!(is_valid_ip("::1"));
337 assert!(is_valid_ip("2001:db8::1"));
338 assert!(!is_valid_ip("not an ip"));
339 assert!(!is_valid_ip("256.1.1.1"));
340 }
341
342 #[test]
343 fn test_is_identifier() {
344 assert!(is_identifier("hello"));
345 assert!(is_identifier("_private"));
346 assert!(is_identifier("camelCase"));
347 assert!(is_identifier("snake_case"));
348 assert!(is_identifier("with123"));
349 assert!(!is_identifier("123start"));
350 assert!(!is_identifier("has-dash"));
351 assert!(!is_identifier(""));
352 }
353
354 #[test]
355 fn test_is_valid_semver() {
356 assert!(is_valid_semver("1.0.0"));
357 assert!(is_valid_semver("v1.0.0"));
358 assert!(is_valid_semver("0.1.0"));
359 assert!(is_valid_semver("1.0.0-alpha"));
360 assert!(!is_valid_semver("1.0"));
361 assert!(!is_valid_semver("1"));
362 assert!(!is_valid_semver("a.b.c"));
363 }
364
365 #[test]
366 fn test_validator() {
367 let mut v = Validator::new();
368 v.check("email", || {
369 if is_valid_email("test@example.com") {
370 Ok(())
371 } else {
372 Err(ValidationError::InvalidPattern {
373 pattern: "email".to_string(),
374 })
375 }
376 });
377 assert!(v.is_valid());
378 assert!(v.finish().is_ok());
379 }
380}