rustapi_validate/v2/rules/
sync_rules.rs1use crate::v2::error::RuleError;
6use crate::v2::traits::ValidationRule;
7use regex::Regex;
8use serde::{Deserialize, Serialize};
9use std::sync::OnceLock;
10
11static EMAIL_REGEX: OnceLock<Regex> = OnceLock::new();
13static URL_REGEX: OnceLock<Regex> = OnceLock::new();
14
15fn email_regex() -> &'static Regex {
16 EMAIL_REGEX.get_or_init(|| {
17 Regex::new(
19 r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
20 ).unwrap()
21 })
22}
23
24fn url_regex() -> &'static Regex {
25 URL_REGEX.get_or_init(|| Regex::new(r"^(https?|ftp)://[^\s/$.?#].[^\s]*$").unwrap())
26}
27
28#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
32pub struct EmailRule {
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub message: Option<String>,
36}
37
38impl EmailRule {
39 pub fn new() -> Self {
41 Self::default()
42 }
43
44 pub fn with_message(message: impl Into<String>) -> Self {
46 Self {
47 message: Some(message.into()),
48 }
49 }
50}
51
52impl ValidationRule<str> for EmailRule {
53 fn validate(&self, value: &str) -> Result<(), RuleError> {
54 if email_regex().is_match(value) {
55 Ok(())
56 } else {
57 let message = self
58 .message
59 .clone()
60 .unwrap_or_else(|| "Invalid email format".to_string());
61 Err(RuleError::new("email", message))
62 }
63 }
64
65 fn rule_name(&self) -> &'static str {
66 "email"
67 }
68}
69
70impl ValidationRule<String> for EmailRule {
71 fn validate(&self, value: &String) -> Result<(), RuleError> {
72 <Self as ValidationRule<str>>::validate(self, value.as_str())
73 }
74
75 fn rule_name(&self) -> &'static str {
76 "email"
77 }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
84pub struct LengthRule {
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub min: Option<usize>,
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub max: Option<usize>,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub message: Option<String>,
94}
95
96impl LengthRule {
97 pub fn new(min: usize, max: usize) -> Self {
99 Self {
100 min: Some(min),
101 max: Some(max),
102 message: None,
103 }
104 }
105
106 pub fn min(min: usize) -> Self {
108 Self {
109 min: Some(min),
110 max: None,
111 message: None,
112 }
113 }
114
115 pub fn max(max: usize) -> Self {
117 Self {
118 min: None,
119 max: Some(max),
120 message: None,
121 }
122 }
123
124 pub fn with_message(mut self, message: impl Into<String>) -> Self {
126 self.message = Some(message.into());
127 self
128 }
129}
130
131impl ValidationRule<str> for LengthRule {
132 fn validate(&self, value: &str) -> Result<(), RuleError> {
133 let len = value.chars().count();
134
135 if let Some(min) = self.min {
136 if len < min {
137 let message = self
138 .message
139 .clone()
140 .unwrap_or_else(|| format!("Length must be at least {min} characters"));
141 return Err(RuleError::new("length", message)
142 .param("min", min)
143 .param("max", self.max)
144 .param("actual", len));
145 }
146 }
147
148 if let Some(max) = self.max {
149 if len > max {
150 let message = self
151 .message
152 .clone()
153 .unwrap_or_else(|| format!("Length must be at most {max} characters"));
154 return Err(RuleError::new("length", message)
155 .param("min", self.min)
156 .param("max", max)
157 .param("actual", len));
158 }
159 }
160
161 Ok(())
162 }
163
164 fn rule_name(&self) -> &'static str {
165 "length"
166 }
167}
168
169impl ValidationRule<String> for LengthRule {
170 fn validate(&self, value: &String) -> Result<(), RuleError> {
171 <Self as ValidationRule<str>>::validate(self, value.as_str())
172 }
173
174 fn rule_name(&self) -> &'static str {
175 "length"
176 }
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
183pub struct RangeRule<T> {
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub min: Option<T>,
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub max: Option<T>,
190 #[serde(skip_serializing_if = "Option::is_none")]
192 pub message: Option<String>,
193}
194
195impl<T> RangeRule<T> {
196 pub fn new(min: T, max: T) -> Self {
198 Self {
199 min: Some(min),
200 max: Some(max),
201 message: None,
202 }
203 }
204
205 pub fn min(min: T) -> Self {
207 Self {
208 min: Some(min),
209 max: None,
210 message: None,
211 }
212 }
213
214 pub fn max(max: T) -> Self {
216 Self {
217 min: None,
218 max: Some(max),
219 message: None,
220 }
221 }
222
223 pub fn with_message(mut self, message: impl Into<String>) -> Self {
225 self.message = Some(message.into());
226 self
227 }
228}
229
230impl<T> ValidationRule<T> for RangeRule<T>
231where
232 T: PartialOrd + std::fmt::Display + Copy + Send + Sync + std::fmt::Debug + Serialize,
233{
234 fn validate(&self, value: &T) -> Result<(), RuleError> {
235 if let Some(ref min) = self.min {
236 if value < min {
237 let message = self
238 .message
239 .clone()
240 .unwrap_or_else(|| format!("Value must be at least {min}"));
241 return Err(RuleError::new("range", message)
242 .param("min", *min)
243 .param("max", self.max)
244 .param("actual", *value));
245 }
246 }
247
248 if let Some(ref max) = self.max {
249 if value > max {
250 let message = self
251 .message
252 .clone()
253 .unwrap_or_else(|| format!("Value must be at most {max}"));
254 return Err(RuleError::new("range", message)
255 .param("min", self.min)
256 .param("max", *max)
257 .param("actual", *value));
258 }
259 }
260
261 Ok(())
262 }
263
264 fn rule_name(&self) -> &'static str {
265 "range"
266 }
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct RegexRule {
274 pub pattern: String,
276 #[serde(skip)]
278 compiled: OnceLock<Regex>,
279 #[serde(skip_serializing_if = "Option::is_none")]
281 pub message: Option<String>,
282}
283
284impl PartialEq for RegexRule {
285 fn eq(&self, other: &Self) -> bool {
286 self.pattern == other.pattern && self.message == other.message
287 }
288}
289
290impl RegexRule {
291 pub fn new(pattern: impl Into<String>) -> Self {
293 Self {
294 pattern: pattern.into(),
295 compiled: OnceLock::new(),
296 message: None,
297 }
298 }
299
300 pub fn with_message(mut self, message: impl Into<String>) -> Self {
302 self.message = Some(message.into());
303 self
304 }
305
306 fn get_regex(&self) -> Result<&Regex, RuleError> {
307 self.compiled.get_or_init(|| {
308 Regex::new(&self.pattern).unwrap_or_else(|_| Regex::new("^$").unwrap())
309 });
310
311 if Regex::new(&self.pattern).is_err() {
313 return Err(RuleError::new(
314 "regex",
315 format!("Invalid regex pattern: {}", self.pattern),
316 ));
317 }
318
319 Ok(self.compiled.get().unwrap())
320 }
321}
322
323impl ValidationRule<str> for RegexRule {
324 fn validate(&self, value: &str) -> Result<(), RuleError> {
325 let regex = self.get_regex()?;
326
327 if regex.is_match(value) {
328 Ok(())
329 } else {
330 let message = self
331 .message
332 .clone()
333 .unwrap_or_else(|| format!("Value does not match pattern: {}", self.pattern));
334 Err(RuleError::new("regex", message).param("pattern", self.pattern.clone()))
335 }
336 }
337
338 fn rule_name(&self) -> &'static str {
339 "regex"
340 }
341}
342
343impl ValidationRule<String> for RegexRule {
344 fn validate(&self, value: &String) -> Result<(), RuleError> {
345 <Self as ValidationRule<str>>::validate(self, value.as_str())
346 }
347
348 fn rule_name(&self) -> &'static str {
349 "regex"
350 }
351}
352
353#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
357pub struct UrlRule {
358 #[serde(skip_serializing_if = "Option::is_none")]
360 pub message: Option<String>,
361}
362
363impl UrlRule {
364 pub fn new() -> Self {
366 Self::default()
367 }
368
369 pub fn with_message(message: impl Into<String>) -> Self {
371 Self {
372 message: Some(message.into()),
373 }
374 }
375}
376
377impl ValidationRule<str> for UrlRule {
378 fn validate(&self, value: &str) -> Result<(), RuleError> {
379 if url_regex().is_match(value) {
380 Ok(())
381 } else {
382 let message = self
383 .message
384 .clone()
385 .unwrap_or_else(|| "Invalid URL format".to_string());
386 Err(RuleError::new("url", message))
387 }
388 }
389
390 fn rule_name(&self) -> &'static str {
391 "url"
392 }
393}
394
395impl ValidationRule<String> for UrlRule {
396 fn validate(&self, value: &String) -> Result<(), RuleError> {
397 <Self as ValidationRule<str>>::validate(self, value.as_str())
398 }
399
400 fn rule_name(&self) -> &'static str {
401 "url"
402 }
403}
404
405#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
409pub struct RequiredRule {
410 #[serde(skip_serializing_if = "Option::is_none")]
412 pub message: Option<String>,
413}
414
415impl RequiredRule {
416 pub fn new() -> Self {
418 Self::default()
419 }
420
421 pub fn with_message(message: impl Into<String>) -> Self {
423 Self {
424 message: Some(message.into()),
425 }
426 }
427}
428
429impl ValidationRule<str> for RequiredRule {
430 fn validate(&self, value: &str) -> Result<(), RuleError> {
431 if !value.trim().is_empty() {
432 Ok(())
433 } else {
434 let message = self
435 .message
436 .clone()
437 .unwrap_or_else(|| "This field is required".to_string());
438 Err(RuleError::new("required", message))
439 }
440 }
441
442 fn rule_name(&self) -> &'static str {
443 "required"
444 }
445}
446
447impl ValidationRule<String> for RequiredRule {
448 fn validate(&self, value: &String) -> Result<(), RuleError> {
449 <Self as ValidationRule<str>>::validate(self, value.as_str())
450 }
451
452 fn rule_name(&self) -> &'static str {
453 "required"
454 }
455}
456
457impl<T> ValidationRule<Option<T>> for RequiredRule
458where
459 T: std::fmt::Debug + Send + Sync,
460{
461 fn validate(&self, value: &Option<T>) -> Result<(), RuleError> {
462 if value.is_some() {
463 Ok(())
464 } else {
465 let message = self
466 .message
467 .clone()
468 .unwrap_or_else(|| "This field is required".to_string());
469 Err(RuleError::new("required", message))
470 }
471 }
472
473 fn rule_name(&self) -> &'static str {
474 "required"
475 }
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481
482 #[test]
483 fn email_rule_valid() {
484 let rule = EmailRule::new();
485 assert!(rule.validate("test@example.com").is_ok());
486 assert!(rule.validate("user.name+tag@domain.co.uk").is_ok());
487 }
488
489 #[test]
490 fn email_rule_invalid() {
491 let rule = EmailRule::new();
492 assert!(rule.validate("invalid").is_err());
493 assert!(rule.validate("@domain.com").is_err());
494 assert!(rule.validate("user@").is_err());
495 }
496
497 #[test]
498 fn email_rule_custom_message() {
499 let rule = EmailRule::with_message("Please enter a valid email");
500 let err = rule.validate("invalid").unwrap_err();
501 assert_eq!(err.message, "Please enter a valid email");
502 }
503
504 #[test]
505 fn length_rule_valid() {
506 let rule = LengthRule::new(3, 10);
507 assert!(rule.validate("abc").is_ok());
508 assert!(rule.validate("abcdefghij").is_ok());
509 }
510
511 #[test]
512 fn length_rule_too_short() {
513 let rule = LengthRule::new(3, 10);
514 let err = rule.validate("ab").unwrap_err();
515 assert_eq!(err.code, "length");
516 }
517
518 #[test]
519 fn length_rule_too_long() {
520 let rule = LengthRule::new(3, 10);
521 let err = rule.validate("abcdefghijk").unwrap_err();
522 assert_eq!(err.code, "length");
523 }
524
525 #[test]
526 fn range_rule_valid() {
527 let rule = RangeRule::new(18, 120);
528 assert!(rule.validate(&18).is_ok());
529 assert!(rule.validate(&50).is_ok());
530 assert!(rule.validate(&120).is_ok());
531 }
532
533 #[test]
534 fn range_rule_too_low() {
535 let rule = RangeRule::new(18, 120);
536 let err = rule.validate(&17).unwrap_err();
537 assert_eq!(err.code, "range");
538 }
539
540 #[test]
541 fn range_rule_too_high() {
542 let rule = RangeRule::new(18, 120);
543 let err = rule.validate(&121).unwrap_err();
544 assert_eq!(err.code, "range");
545 }
546
547 #[test]
548 fn regex_rule_valid() {
549 let rule = RegexRule::new(r"^\d{3}-\d{4}$");
550 assert!(rule.validate("123-4567").is_ok());
551 }
552
553 #[test]
554 fn regex_rule_invalid() {
555 let rule = RegexRule::new(r"^\d{3}-\d{4}$");
556 assert!(rule.validate("1234567").is_err());
557 }
558
559 #[test]
560 fn url_rule_valid() {
561 let rule = UrlRule::new();
562 assert!(rule.validate("https://example.com").is_ok());
563 assert!(rule.validate("http://example.com/path?query=1").is_ok());
564 }
565
566 #[test]
567 fn url_rule_invalid() {
568 let rule = UrlRule::new();
569 assert!(rule.validate("not-a-url").is_err());
570 assert!(rule.validate("ftp://").is_err());
571 }
572
573 #[test]
574 fn required_rule_valid() {
575 let rule = RequiredRule::new();
576 assert!(rule.validate("value").is_ok());
577 assert!(rule.validate(" value ").is_ok());
578 }
579
580 #[test]
581 fn required_rule_empty() {
582 let rule = RequiredRule::new();
583 assert!(rule.validate("").is_err());
584 assert!(rule.validate(" ").is_err());
585 }
586
587 #[test]
588 fn required_rule_option() {
589 let rule = RequiredRule::new();
590 assert!(ValidationRule::<Option<i32>>::validate(&rule, &Some(42)).is_ok());
591 assert!(ValidationRule::<Option<i32>>::validate(&rule, &None).is_err());
592 }
593
594 #[test]
595 fn rule_serialization_roundtrip() {
596 let rule = LengthRule::new(3, 50).with_message("Custom message");
597 let json = serde_json::to_string(&rule).unwrap();
598 let parsed: LengthRule = serde_json::from_str(&json).unwrap();
599 assert_eq!(rule, parsed);
600 }
601}