rustapi_validate/v2/
traits.rs

1//! Core validation traits for the v2 validation engine.
2
3use crate::v2::context::ValidationContext;
4use crate::v2::error::{RuleError, ValidationErrors};
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7use std::fmt::Debug;
8
9/// Trait for synchronous validation of a struct.
10///
11/// Implement this trait to enable validation on your types.
12///
13/// ## Example
14///
15/// ```rust,ignore
16/// use rustapi_validate::v2::prelude::*;
17///
18/// struct User {
19///     email: String,
20///     age: u8,
21/// }
22///
23/// impl Validate for User {
24///     fn validate(&self) -> Result<(), ValidationErrors> {
25///         let mut errors = ValidationErrors::new();
26///         
27///         if let Err(e) = EmailRule::default().validate(&self.email) {
28///             errors.add("email", e);
29///         }
30///         
31///         if let Err(e) = RangeRule::new(18, 120).validate(&self.age) {
32///             errors.add("age", e);
33///         }
34///         
35///         errors.into_result()
36///     }
37/// }
38/// ```
39pub trait Validate {
40    /// Validate the struct synchronously.
41    ///
42    /// Returns `Ok(())` if validation passes, or `Err(ValidationErrors)` with all field errors.
43    fn validate(&self) -> Result<(), ValidationErrors>;
44
45    /// Validate and return the struct if valid.
46    fn validated(self) -> Result<Self, ValidationErrors>
47    where
48        Self: Sized,
49    {
50        self.validate()?;
51        Ok(self)
52    }
53}
54
55/// Trait for asynchronous validation of a struct.
56///
57/// Use this trait when validation requires async operations like database checks or API calls.
58///
59/// ## Example
60///
61/// ```rust,ignore
62/// use rustapi_validate::v2::prelude::*;
63///
64/// struct CreateUser {
65///     email: String,
66/// }
67///
68/// #[async_trait]
69/// impl AsyncValidate for CreateUser {
70///     async fn validate_async(&self, ctx: &ValidationContext) -> Result<(), ValidationErrors> {
71///         let mut errors = ValidationErrors::new();
72///         
73///         // Check email uniqueness in database
74///         if let Some(db) = ctx.database() {
75///             let rule = AsyncUniqueRule::new("users", "email");
76///             if let Err(e) = rule.validate_async(&self.email, ctx).await {
77///                 errors.add("email", e);
78///             }
79///         }
80///         
81///         errors.into_result()
82///     }
83/// }
84/// ```
85#[async_trait]
86pub trait AsyncValidate: Validate + Send + Sync {
87    /// Validate the struct asynchronously.
88    ///
89    /// This method is called after `validate()` and can perform async operations
90    /// like database queries or external API calls.
91    async fn validate_async(&self, ctx: &ValidationContext) -> Result<(), ValidationErrors>;
92
93    /// Perform full validation (sync + async).
94    async fn validate_full(&self, ctx: &ValidationContext) -> Result<(), ValidationErrors> {
95        // First run sync validation
96        self.validate()?;
97        // Then run async validation
98        self.validate_async(ctx).await
99    }
100
101    /// Validate and return the struct if valid (async version).
102    async fn validated_async(self, ctx: &ValidationContext) -> Result<Self, ValidationErrors>
103    where
104        Self: Sized,
105    {
106        self.validate_full(ctx).await?;
107        Ok(self)
108    }
109}
110
111/// Trait for individual validation rules.
112///
113/// Each rule validates a single value and returns a `RuleError` on failure.
114/// Rules should be serializable for configuration and pretty-printing.
115///
116/// ## Example
117///
118/// ```rust,ignore
119/// use rustapi_validate::v2::prelude::*;
120///
121/// struct PositiveRule;
122///
123/// impl ValidationRule<i32> for PositiveRule {
124///     fn validate(&self, value: &i32) -> Result<(), RuleError> {
125///         if *value > 0 {
126///             Ok(())
127///         } else {
128///             Err(RuleError::new("positive", "Value must be positive"))
129///         }
130///     }
131///     
132///     fn rule_name(&self) -> &'static str {
133///         "positive"
134///     }
135/// }
136/// ```
137pub trait ValidationRule<T: ?Sized>: Debug + Send + Sync {
138    /// Validate the value against this rule.
139    fn validate(&self, value: &T) -> Result<(), RuleError>;
140
141    /// Get the rule name/code for error reporting.
142    fn rule_name(&self) -> &'static str;
143
144    /// Get the default error message for this rule.
145    fn default_message(&self) -> String {
146        format!("Validation failed for rule '{}'", self.rule_name())
147    }
148}
149
150/// Trait for async validation rules.
151///
152/// Use this for rules that require async operations like database or API checks.
153#[async_trait]
154pub trait AsyncValidationRule<T: ?Sized + Sync>: Debug + Send + Sync {
155    /// Validate the value asynchronously.
156    async fn validate_async(&self, value: &T, ctx: &ValidationContext) -> Result<(), RuleError>;
157
158    /// Get the rule name/code for error reporting.
159    fn rule_name(&self) -> &'static str;
160
161    /// Get the default error message for this rule.
162    fn default_message(&self) -> String {
163        format!("Async validation failed for rule '{}'", self.rule_name())
164    }
165}
166
167/// Wrapper for serializable validation rules.
168///
169/// This enum allows rules to be serialized/deserialized for configuration files
170/// and pretty-printing.
171#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
172#[serde(tag = "type", rename_all = "snake_case")]
173pub enum SerializableRule {
174    /// Email format validation
175    Email {
176        #[serde(skip_serializing_if = "Option::is_none")]
177        message: Option<String>,
178    },
179    /// String length validation
180    Length {
181        #[serde(skip_serializing_if = "Option::is_none")]
182        min: Option<usize>,
183        #[serde(skip_serializing_if = "Option::is_none")]
184        max: Option<usize>,
185        #[serde(skip_serializing_if = "Option::is_none")]
186        message: Option<String>,
187    },
188    /// Numeric range validation
189    Range {
190        #[serde(skip_serializing_if = "Option::is_none")]
191        min: Option<f64>,
192        #[serde(skip_serializing_if = "Option::is_none")]
193        max: Option<f64>,
194        #[serde(skip_serializing_if = "Option::is_none")]
195        message: Option<String>,
196    },
197    /// Regex pattern validation
198    Regex {
199        pattern: String,
200        #[serde(skip_serializing_if = "Option::is_none")]
201        message: Option<String>,
202    },
203    /// URL format validation
204    Url {
205        #[serde(skip_serializing_if = "Option::is_none")]
206        message: Option<String>,
207    },
208    /// Required (non-empty) validation
209    Required {
210        #[serde(skip_serializing_if = "Option::is_none")]
211        message: Option<String>,
212    },
213    /// Database uniqueness check (async)
214    AsyncUnique {
215        table: String,
216        column: String,
217        #[serde(skip_serializing_if = "Option::is_none")]
218        message: Option<String>,
219    },
220    /// Database existence check (async)
221    AsyncExists {
222        table: String,
223        column: String,
224        #[serde(skip_serializing_if = "Option::is_none")]
225        message: Option<String>,
226    },
227    /// External API validation (async)
228    AsyncApi {
229        endpoint: String,
230        #[serde(skip_serializing_if = "Option::is_none")]
231        message: Option<String>,
232    },
233}
234
235impl SerializableRule {
236    /// Pretty print the rule definition.
237    pub fn pretty_print(&self) -> String {
238        match self {
239            SerializableRule::Email { message } => {
240                let msg = message
241                    .as_ref()
242                    .map(|m| format!(", message = \"{}\"", m))
243                    .unwrap_or_default();
244                format!("#[validate(email{})]", msg)
245            }
246            SerializableRule::Length { min, max, message } => {
247                let mut parts = Vec::new();
248                if let Some(min) = min {
249                    parts.push(format!("min = {}", min));
250                }
251                if let Some(max) = max {
252                    parts.push(format!("max = {}", max));
253                }
254                if let Some(msg) = message {
255                    parts.push(format!("message = \"{}\"", msg));
256                }
257                format!("#[validate(length({}))]", parts.join(", "))
258            }
259            SerializableRule::Range { min, max, message } => {
260                let mut parts = Vec::new();
261                if let Some(min) = min {
262                    parts.push(format!("min = {}", min));
263                }
264                if let Some(max) = max {
265                    parts.push(format!("max = {}", max));
266                }
267                if let Some(msg) = message {
268                    parts.push(format!("message = \"{}\"", msg));
269                }
270                format!("#[validate(range({}))]", parts.join(", "))
271            }
272            SerializableRule::Regex { pattern, message } => {
273                let msg = message
274                    .as_ref()
275                    .map(|m| format!(", message = \"{}\"", m))
276                    .unwrap_or_default();
277                format!("#[validate(regex = \"{}\"{})]", pattern, msg)
278            }
279            SerializableRule::Url { message } => {
280                let msg = message
281                    .as_ref()
282                    .map(|m| format!(", message = \"{}\"", m))
283                    .unwrap_or_default();
284                format!("#[validate(url{})]", msg)
285            }
286            SerializableRule::Required { message } => {
287                let msg = message
288                    .as_ref()
289                    .map(|m| format!(", message = \"{}\"", m))
290                    .unwrap_or_default();
291                format!("#[validate(required{})]", msg)
292            }
293            SerializableRule::AsyncUnique {
294                table,
295                column,
296                message,
297            } => {
298                let msg = message
299                    .as_ref()
300                    .map(|m| format!(", message = \"{}\"", m))
301                    .unwrap_or_default();
302                format!(
303                    "#[validate(async_unique(table = \"{}\", column = \"{}\"{}))]",
304                    table, column, msg
305                )
306            }
307            SerializableRule::AsyncExists {
308                table,
309                column,
310                message,
311            } => {
312                let msg = message
313                    .as_ref()
314                    .map(|m| format!(", message = \"{}\"", m))
315                    .unwrap_or_default();
316                format!(
317                    "#[validate(async_exists(table = \"{}\", column = \"{}\"{}))]",
318                    table, column, msg
319                )
320            }
321            SerializableRule::AsyncApi { endpoint, message } => {
322                let msg = message
323                    .as_ref()
324                    .map(|m| format!(", message = \"{}\"", m))
325                    .unwrap_or_default();
326                format!("#[validate(async_api(endpoint = \"{}\"{}))]", endpoint, msg)
327            }
328        }
329    }
330
331    /// Parse a SerializableRule from a pretty-printed string.
332    ///
333    /// This is the inverse of `pretty_print()` and enables round-trip
334    /// serialization of validation rules.
335    pub fn parse(s: &str) -> Option<Self> {
336        let s = s.trim();
337
338        // Must start with #[validate( and end with )]
339        if !s.starts_with("#[validate(") || !s.ends_with(")]") {
340            return None;
341        }
342
343        // Extract the inner content
344        let inner = &s[11..s.len() - 2];
345
346        // Parse based on rule type
347        if inner == "email" || inner.starts_with("email,") {
348            let message = Self::extract_message(inner);
349            return Some(SerializableRule::Email { message });
350        }
351
352        if inner == "url" || inner.starts_with("url,") {
353            let message = Self::extract_message(inner);
354            return Some(SerializableRule::Url { message });
355        }
356
357        if inner == "required" || inner.starts_with("required,") {
358            let message = Self::extract_message(inner);
359            return Some(SerializableRule::Required { message });
360        }
361
362        if inner.starts_with("length(") {
363            return Self::parse_length(inner);
364        }
365
366        if inner.starts_with("range(") {
367            return Self::parse_range(inner);
368        }
369
370        if inner.starts_with("regex") {
371            return Self::parse_regex(inner);
372        }
373
374        if inner.starts_with("async_unique(") {
375            return Self::parse_async_unique(inner);
376        }
377
378        if inner.starts_with("async_exists(") {
379            return Self::parse_async_exists(inner);
380        }
381
382        if inner.starts_with("async_api(") {
383            return Self::parse_async_api(inner);
384        }
385
386        None
387    }
388
389    fn extract_message(s: &str) -> Option<String> {
390        if let Some(idx) = s.find("message = \"") {
391            let start = idx + 11;
392            if let Some(end) = s[start..].find('"') {
393                return Some(s[start..start + end].to_string());
394            }
395        }
396        None
397    }
398
399    fn extract_param(s: &str, param: &str) -> Option<String> {
400        let pattern = format!("{} = ", param);
401        if let Some(idx) = s.find(&pattern) {
402            let start = idx + pattern.len();
403            let rest = &s[start..];
404
405            // Check if it's a quoted string
406            if let Some(stripped) = rest.strip_prefix('"') {
407                if let Some(end) = stripped.find('"') {
408                    return Some(stripped[..end].to_string());
409                }
410            } else {
411                // It's a number or other value
412                let end = rest.find([',', ')']).unwrap_or(rest.len());
413                return Some(rest[..end].trim().to_string());
414            }
415        }
416        None
417    }
418
419    fn parse_length(s: &str) -> Option<Self> {
420        let min = Self::extract_param(s, "min").and_then(|v| v.parse().ok());
421        let max = Self::extract_param(s, "max").and_then(|v| v.parse().ok());
422        let message = Self::extract_message(s);
423        Some(SerializableRule::Length { min, max, message })
424    }
425
426    fn parse_range(s: &str) -> Option<Self> {
427        let min = Self::extract_param(s, "min").and_then(|v| v.parse().ok());
428        let max = Self::extract_param(s, "max").and_then(|v| v.parse().ok());
429        let message = Self::extract_message(s);
430        Some(SerializableRule::Range { min, max, message })
431    }
432
433    fn parse_regex(s: &str) -> Option<Self> {
434        let pattern =
435            Self::extract_param(s, "regex").or_else(|| Self::extract_param(s, "pattern"))?;
436        let message = Self::extract_message(s);
437        Some(SerializableRule::Regex { pattern, message })
438    }
439
440    fn parse_async_unique(s: &str) -> Option<Self> {
441        let table = Self::extract_param(s, "table")?;
442        let column = Self::extract_param(s, "column")?;
443        let message = Self::extract_message(s);
444        Some(SerializableRule::AsyncUnique {
445            table,
446            column,
447            message,
448        })
449    }
450
451    fn parse_async_exists(s: &str) -> Option<Self> {
452        let table = Self::extract_param(s, "table")?;
453        let column = Self::extract_param(s, "column")?;
454        let message = Self::extract_message(s);
455        Some(SerializableRule::AsyncExists {
456            table,
457            column,
458            message,
459        })
460    }
461
462    fn parse_async_api(s: &str) -> Option<Self> {
463        let endpoint = Self::extract_param(s, "endpoint")?;
464        let message = Self::extract_message(s);
465        Some(SerializableRule::AsyncApi { endpoint, message })
466    }
467}
468
469// Conversion implementations from concrete rules to SerializableRule
470use crate::v2::rules::{
471    AsyncApiRule, AsyncExistsRule, AsyncUniqueRule, EmailRule, LengthRule, RegexRule, RequiredRule,
472    UrlRule,
473};
474
475impl From<EmailRule> for SerializableRule {
476    fn from(rule: EmailRule) -> Self {
477        SerializableRule::Email {
478            message: rule.message,
479        }
480    }
481}
482
483impl From<LengthRule> for SerializableRule {
484    fn from(rule: LengthRule) -> Self {
485        SerializableRule::Length {
486            min: rule.min,
487            max: rule.max,
488            message: rule.message,
489        }
490    }
491}
492
493impl From<RegexRule> for SerializableRule {
494    fn from(rule: RegexRule) -> Self {
495        SerializableRule::Regex {
496            pattern: rule.pattern,
497            message: rule.message,
498        }
499    }
500}
501
502impl From<UrlRule> for SerializableRule {
503    fn from(rule: UrlRule) -> Self {
504        SerializableRule::Url {
505            message: rule.message,
506        }
507    }
508}
509
510impl From<RequiredRule> for SerializableRule {
511    fn from(rule: RequiredRule) -> Self {
512        SerializableRule::Required {
513            message: rule.message,
514        }
515    }
516}
517
518impl From<AsyncUniqueRule> for SerializableRule {
519    fn from(rule: AsyncUniqueRule) -> Self {
520        SerializableRule::AsyncUnique {
521            table: rule.table,
522            column: rule.column,
523            message: rule.message,
524        }
525    }
526}
527
528impl From<AsyncExistsRule> for SerializableRule {
529    fn from(rule: AsyncExistsRule) -> Self {
530        SerializableRule::AsyncExists {
531            table: rule.table,
532            column: rule.column,
533            message: rule.message,
534        }
535    }
536}
537
538impl From<AsyncApiRule> for SerializableRule {
539    fn from(rule: AsyncApiRule) -> Self {
540        SerializableRule::AsyncApi {
541            endpoint: rule.endpoint,
542            message: rule.message,
543        }
544    }
545}
546
547#[cfg(test)]
548mod tests {
549    use super::*;
550
551    #[test]
552    fn serializable_rule_email_pretty_print() {
553        let rule = SerializableRule::Email { message: None };
554        assert_eq!(rule.pretty_print(), "#[validate(email)]");
555
556        let rule = SerializableRule::Email {
557            message: Some("Invalid email".to_string()),
558        };
559        assert_eq!(
560            rule.pretty_print(),
561            "#[validate(email, message = \"Invalid email\")]"
562        );
563    }
564
565    #[test]
566    fn serializable_rule_length_pretty_print() {
567        let rule = SerializableRule::Length {
568            min: Some(3),
569            max: Some(50),
570            message: None,
571        };
572        assert_eq!(
573            rule.pretty_print(),
574            "#[validate(length(min = 3, max = 50))]"
575        );
576    }
577
578    #[test]
579    fn serializable_rule_roundtrip() {
580        let rule = SerializableRule::Range {
581            min: Some(18.0),
582            max: Some(120.0),
583            message: Some("Age must be between 18 and 120".to_string()),
584        };
585
586        let json = serde_json::to_string(&rule).unwrap();
587        let parsed: SerializableRule = serde_json::from_str(&json).unwrap();
588        assert_eq!(rule, parsed);
589    }
590
591    #[test]
592    fn serializable_rule_pretty_print_roundtrip_email() {
593        let rule = SerializableRule::Email { message: None };
594        let pretty = rule.pretty_print();
595        let parsed = SerializableRule::parse(&pretty).unwrap();
596        assert_eq!(rule, parsed);
597
598        let rule = SerializableRule::Email {
599            message: Some("Invalid email".to_string()),
600        };
601        let pretty = rule.pretty_print();
602        let parsed = SerializableRule::parse(&pretty).unwrap();
603        assert_eq!(rule, parsed);
604    }
605
606    #[test]
607    fn serializable_rule_pretty_print_roundtrip_length() {
608        let rule = SerializableRule::Length {
609            min: Some(3),
610            max: Some(50),
611            message: None,
612        };
613        let pretty = rule.pretty_print();
614        let parsed = SerializableRule::parse(&pretty).unwrap();
615        assert_eq!(rule, parsed);
616    }
617
618    #[test]
619    fn serializable_rule_pretty_print_roundtrip_range() {
620        let rule = SerializableRule::Range {
621            min: Some(18.0),
622            max: Some(120.0),
623            message: None,
624        };
625        let pretty = rule.pretty_print();
626        let parsed = SerializableRule::parse(&pretty).unwrap();
627        assert_eq!(rule, parsed);
628    }
629
630    #[test]
631    fn serializable_rule_pretty_print_roundtrip_url() {
632        let rule = SerializableRule::Url { message: None };
633        let pretty = rule.pretty_print();
634        let parsed = SerializableRule::parse(&pretty).unwrap();
635        assert_eq!(rule, parsed);
636    }
637
638    #[test]
639    fn serializable_rule_pretty_print_roundtrip_required() {
640        let rule = SerializableRule::Required { message: None };
641        let pretty = rule.pretty_print();
642        let parsed = SerializableRule::parse(&pretty).unwrap();
643        assert_eq!(rule, parsed);
644    }
645
646    #[test]
647    fn serializable_rule_pretty_print_roundtrip_async_unique() {
648        let rule = SerializableRule::AsyncUnique {
649            table: "users".to_string(),
650            column: "email".to_string(),
651            message: None,
652        };
653        let pretty = rule.pretty_print();
654        let parsed = SerializableRule::parse(&pretty).unwrap();
655        assert_eq!(rule, parsed);
656    }
657
658    #[test]
659    fn serializable_rule_pretty_print_roundtrip_async_exists() {
660        let rule = SerializableRule::AsyncExists {
661            table: "categories".to_string(),
662            column: "id".to_string(),
663            message: Some("Category not found".to_string()),
664        };
665        let pretty = rule.pretty_print();
666        let parsed = SerializableRule::parse(&pretty).unwrap();
667        assert_eq!(rule, parsed);
668    }
669
670    #[test]
671    fn serializable_rule_pretty_print_roundtrip_async_api() {
672        let rule = SerializableRule::AsyncApi {
673            endpoint: "https://api.example.com/validate".to_string(),
674            message: None,
675        };
676        let pretty = rule.pretty_print();
677        let parsed = SerializableRule::parse(&pretty).unwrap();
678        assert_eq!(rule, parsed);
679    }
680
681    #[test]
682    fn from_email_rule() {
683        let rule = EmailRule::with_message("Invalid email");
684        let serializable: SerializableRule = rule.into();
685        assert_eq!(
686            serializable,
687            SerializableRule::Email {
688                message: Some("Invalid email".to_string())
689            }
690        );
691    }
692
693    #[test]
694    fn from_length_rule() {
695        let rule = LengthRule::new(3, 50).with_message("Invalid length");
696        let serializable: SerializableRule = rule.into();
697        assert_eq!(
698            serializable,
699            SerializableRule::Length {
700                min: Some(3),
701                max: Some(50),
702                message: Some("Invalid length".to_string())
703            }
704        );
705    }
706
707    #[test]
708    fn from_async_unique_rule() {
709        let rule = AsyncUniqueRule::new("users", "email").with_message("Email taken");
710        let serializable: SerializableRule = rule.into();
711        assert_eq!(
712            serializable,
713            SerializableRule::AsyncUnique {
714                table: "users".to_string(),
715                column: "email".to_string(),
716                message: Some("Email taken".to_string())
717            }
718        );
719    }
720}