rustapi_validate/v2/rules/
sync_rules.rs

1//! Synchronous validation rules.
2//!
3//! These rules perform validation without requiring async operations.
4
5use crate::v2::error::RuleError;
6use crate::v2::traits::ValidationRule;
7use regex::Regex;
8use serde::{Deserialize, Serialize};
9use std::sync::OnceLock;
10
11// Pre-compiled regex patterns
12static 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        // RFC 5322 simplified email regex
18        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/// Email format validation rule.
29///
30/// Validates that a string is a valid email address according to RFC 5322.
31#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
32pub struct EmailRule {
33    /// Custom error message
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub message: Option<String>,
36}
37
38impl EmailRule {
39    /// Create a new email rule with default message.
40    pub fn new() -> Self {
41        Self::default()
42    }
43
44    /// Create an email rule with a custom message.
45    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/// String length validation rule.
81///
82/// Validates that a string's length is within specified bounds.
83#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
84pub struct LengthRule {
85    /// Minimum length (inclusive)
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub min: Option<usize>,
88    /// Maximum length (inclusive)
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub max: Option<usize>,
91    /// Custom error message
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub message: Option<String>,
94}
95
96impl LengthRule {
97    /// Create a length rule with min and max bounds.
98    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    /// Create a length rule with only a minimum.
107    pub fn min(min: usize) -> Self {
108        Self {
109            min: Some(min),
110            max: None,
111            message: None,
112        }
113    }
114
115    /// Create a length rule with only a maximum.
116    pub fn max(max: usize) -> Self {
117        Self {
118            min: None,
119            max: Some(max),
120            message: None,
121        }
122    }
123
124    /// Set a custom error message.
125    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/// Numeric range validation rule.
180///
181/// Validates that a number is within specified bounds.
182#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
183pub struct RangeRule<T> {
184    /// Minimum value (inclusive)
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub min: Option<T>,
187    /// Maximum value (inclusive)
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub max: Option<T>,
190    /// Custom error message
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub message: Option<String>,
193}
194
195impl<T> RangeRule<T> {
196    /// Create a range rule with min and max bounds.
197    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    /// Create a range rule with only a minimum.
206    pub fn min(min: T) -> Self {
207        Self {
208            min: Some(min),
209            max: None,
210            message: None,
211        }
212    }
213
214    /// Create a range rule with only a maximum.
215    pub fn max(max: T) -> Self {
216        Self {
217            min: None,
218            max: Some(max),
219            message: None,
220        }
221    }
222
223    /// Set a custom error message.
224    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/// Regex pattern validation rule.
270///
271/// Validates that a string matches a regex pattern.
272#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct RegexRule {
274    /// The regex pattern
275    pub pattern: String,
276    /// Compiled regex (not serialized)
277    #[serde(skip)]
278    compiled: OnceLock<Regex>,
279    /// Custom error message
280    #[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    /// Create a new regex rule.
292    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    /// Set a custom error message.
301    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        // Verify the pattern is valid
312        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/// URL format validation rule.
354///
355/// Validates that a string is a valid URL.
356#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
357pub struct UrlRule {
358    /// Custom error message
359    #[serde(skip_serializing_if = "Option::is_none")]
360    pub message: Option<String>,
361}
362
363impl UrlRule {
364    /// Create a new URL rule.
365    pub fn new() -> Self {
366        Self::default()
367    }
368
369    /// Create a URL rule with a custom message.
370    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/// Required (non-empty) validation rule.
406///
407/// Validates that a value is not empty.
408#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
409pub struct RequiredRule {
410    /// Custom error message
411    #[serde(skip_serializing_if = "Option::is_none")]
412    pub message: Option<String>,
413}
414
415impl RequiredRule {
416    /// Create a new required rule.
417    pub fn new() -> Self {
418        Self::default()
419    }
420
421    /// Create a required rule with a custom message.
422    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}