Skip to main content

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 with the default group.
41    fn validate(&self) -> Result<(), ValidationErrors> {
42        self.validate_with_group(crate::v2::group::ValidationGroup::Default)
43    }
44
45    /// Validate the struct with a specific validation group.
46    fn validate_with_group(
47        &self,
48        group: crate::v2::group::ValidationGroup,
49    ) -> Result<(), ValidationErrors>;
50
51    /// Validate and return the struct if valid.
52    fn validated(self) -> Result<Self, ValidationErrors>
53    where
54        Self: Sized,
55    {
56        self.validate()?;
57        Ok(self)
58    }
59
60    /// Validate and return the struct if valid (with group).
61    fn validated_with_group(
62        self,
63        group: crate::v2::group::ValidationGroup,
64    ) -> Result<Self, ValidationErrors>
65    where
66        Self: Sized,
67    {
68        self.validate_with_group(group)?;
69        Ok(self)
70    }
71}
72
73/// Trait for asynchronous validation of a struct.
74///
75/// Use this trait when validation requires async operations like database checks or API calls.
76///
77/// ## Example
78///
79/// ```rust,ignore
80/// use rustapi_validate::v2::prelude::*;
81///
82/// struct CreateUser {
83///     email: String,
84/// }
85///
86/// #[async_trait]
87/// impl AsyncValidate for CreateUser {
88///     async fn validate_async_with_group(&self, ctx: &ValidationContext, group: ValidationGroup) -> Result<(), ValidationErrors> {
89///         let mut errors = ValidationErrors::new();
90///         
91///         // Check email uniqueness in database
92///         if let Some(db) = ctx.database() {
93///             let rule = AsyncUniqueRule::new("users", "email");
94///             if let Err(e) = rule.validate_async(&self.email, ctx).await {
95///                 errors.add("email", e);
96///             }
97///         }
98///         
99///         errors.into_result()
100///     }
101/// }
102/// ```
103#[async_trait]
104pub trait AsyncValidate: Validate + Send + Sync {
105    /// Validate the struct asynchronously with the default group.
106    async fn validate_async(&self, ctx: &ValidationContext) -> Result<(), ValidationErrors> {
107        self.validate_async_with_group(ctx, crate::v2::group::ValidationGroup::Default)
108            .await
109    }
110
111    /// Validate the struct asynchronously with a specific group.
112    async fn validate_async_with_group(
113        &self,
114        ctx: &ValidationContext,
115        group: crate::v2::group::ValidationGroup,
116    ) -> Result<(), ValidationErrors>;
117
118    /// Perform full validation (sync + async) with default group.
119    async fn validate_full(&self, ctx: &ValidationContext) -> Result<(), ValidationErrors> {
120        self.validate_full_with_group(ctx, crate::v2::group::ValidationGroup::Default)
121            .await
122    }
123
124    /// Perform full validation (sync + async) with specific group.
125    async fn validate_full_with_group(
126        &self,
127        ctx: &ValidationContext,
128        group: crate::v2::group::ValidationGroup,
129    ) -> Result<(), ValidationErrors> {
130        // First run sync validation
131        self.validate_with_group(group.clone())?;
132        // Then run async validation
133        self.validate_async_with_group(ctx, group).await
134    }
135
136    /// Validate and return the struct if valid (async version).
137    async fn validated_async(self, ctx: &ValidationContext) -> Result<Self, ValidationErrors>
138    where
139        Self: Sized,
140    {
141        self.validate_full(ctx).await?;
142        Ok(self)
143    }
144
145    /// Validate and return the struct if valid (async version with group).
146    async fn validated_async_with_group(
147        self,
148        ctx: &ValidationContext,
149        group: crate::v2::group::ValidationGroup,
150    ) -> Result<Self, ValidationErrors>
151    where
152        Self: Sized,
153    {
154        self.validate_full_with_group(ctx, group).await?;
155        Ok(self)
156    }
157}
158
159/// Trait for individual validation rules.
160///
161/// Each rule validates a single value and returns a `RuleError` on failure.
162/// Rules should be serializable for configuration and pretty-printing.
163///
164/// ## Example
165///
166/// ```rust,ignore
167/// use rustapi_validate::v2::prelude::*;
168///
169/// struct PositiveRule;
170///
171/// impl ValidationRule<i32> for PositiveRule {
172///     fn validate(&self, value: &i32) -> Result<(), RuleError> {
173///         if *value > 0 {
174///             Ok(())
175///         } else {
176///             Err(RuleError::new("positive", "Value must be positive"))
177///         }
178///     }
179///     
180///     fn rule_name(&self) -> &'static str {
181///         "positive"
182///     }
183/// }
184/// ```
185pub trait ValidationRule<T: ?Sized>: Debug + Send + Sync {
186    /// Validate the value against this rule.
187    fn validate(&self, value: &T) -> Result<(), RuleError>;
188
189    /// Get the rule name/code for error reporting.
190    fn rule_name(&self) -> &'static str;
191
192    /// Get the default error message for this rule.
193    fn default_message(&self) -> String {
194        format!("Validation failed for rule '{}'", self.rule_name())
195    }
196}
197
198/// Trait for async validation rules.
199///
200/// Use this for rules that require async operations like database or API checks.
201#[async_trait]
202pub trait AsyncValidationRule<T: ?Sized + Sync>: Debug + Send + Sync {
203    /// Validate the value asynchronously.
204    async fn validate_async(&self, value: &T, ctx: &ValidationContext) -> Result<(), RuleError>;
205
206    /// Get the rule name/code for error reporting.
207    fn rule_name(&self) -> &'static str;
208
209    /// Get the default error message for this rule.
210    fn default_message(&self) -> String {
211        format!("Async validation failed for rule '{}'", self.rule_name())
212    }
213}
214
215/// Wrapper for serializable validation rules.
216///
217/// This enum allows rules to be serialized/deserialized for configuration files
218/// and pretty-printing.
219#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
220#[serde(tag = "type", rename_all = "snake_case")]
221pub enum SerializableRule {
222    /// Email format validation
223    Email {
224        #[serde(skip_serializing_if = "Option::is_none")]
225        message: Option<String>,
226    },
227    /// String length validation
228    Length {
229        #[serde(skip_serializing_if = "Option::is_none")]
230        min: Option<usize>,
231        #[serde(skip_serializing_if = "Option::is_none")]
232        max: Option<usize>,
233        #[serde(skip_serializing_if = "Option::is_none")]
234        message: Option<String>,
235    },
236    /// Numeric range validation
237    Range {
238        #[serde(skip_serializing_if = "Option::is_none")]
239        min: Option<f64>,
240        #[serde(skip_serializing_if = "Option::is_none")]
241        max: Option<f64>,
242        #[serde(skip_serializing_if = "Option::is_none")]
243        message: Option<String>,
244    },
245    /// Regex pattern validation
246    Regex {
247        pattern: String,
248        #[serde(skip_serializing_if = "Option::is_none")]
249        message: Option<String>,
250    },
251    /// URL format validation
252    Url {
253        #[serde(skip_serializing_if = "Option::is_none")]
254        message: Option<String>,
255    },
256    /// Required (non-empty) validation
257    Required {
258        #[serde(skip_serializing_if = "Option::is_none")]
259        message: Option<String>,
260    },
261    /// Database uniqueness check (async)
262    AsyncUnique {
263        table: String,
264        column: String,
265        #[serde(skip_serializing_if = "Option::is_none")]
266        message: Option<String>,
267    },
268    /// Database existence check (async)
269    AsyncExists {
270        table: String,
271        column: String,
272        #[serde(skip_serializing_if = "Option::is_none")]
273        message: Option<String>,
274    },
275    /// External API validation (async)
276    AsyncApi {
277        endpoint: String,
278        #[serde(skip_serializing_if = "Option::is_none")]
279        message: Option<String>,
280    },
281    /// Credit Card validation
282    CreditCard {
283        #[serde(skip_serializing_if = "Option::is_none")]
284        message: Option<String>,
285    },
286    /// IP Address validation
287    Ip {
288        #[serde(skip_serializing_if = "Option::is_none")]
289        v4: Option<bool>,
290        #[serde(skip_serializing_if = "Option::is_none")]
291        v6: Option<bool>,
292        #[serde(skip_serializing_if = "Option::is_none")]
293        message: Option<String>,
294    },
295    /// Phone number validation
296    Phone {
297        #[serde(skip_serializing_if = "Option::is_none")]
298        message: Option<String>,
299    },
300    /// Contains substring validation
301    Contains {
302        needle: String,
303        #[serde(skip_serializing_if = "Option::is_none")]
304        message: Option<String>,
305    },
306    /// Custom async validation function
307    CustomAsync {
308        function: String,
309        #[serde(skip_serializing_if = "Option::is_none")]
310        message: Option<String>,
311    },
312}
313
314impl SerializableRule {
315    /// Pretty print the rule definition.
316    pub fn pretty_print(&self) -> String {
317        match self {
318            SerializableRule::Email { message } => {
319                let msg = message
320                    .as_ref()
321                    .map(|m| format!(", message = \"{}\"", m))
322                    .unwrap_or_default();
323                format!("#[validate(email{})]", msg)
324            }
325            SerializableRule::Length { min, max, message } => {
326                let mut parts = Vec::new();
327                if let Some(min) = min {
328                    parts.push(format!("min = {}", min));
329                }
330                if let Some(max) = max {
331                    parts.push(format!("max = {}", max));
332                }
333                if let Some(msg) = message {
334                    parts.push(format!("message = \"{}\"", msg));
335                }
336                format!("#[validate(length({}))]", parts.join(", "))
337            }
338            SerializableRule::Range { min, max, message } => {
339                let mut parts = Vec::new();
340                if let Some(min) = min {
341                    parts.push(format!("min = {}", min));
342                }
343                if let Some(max) = max {
344                    parts.push(format!("max = {}", max));
345                }
346                if let Some(msg) = message {
347                    parts.push(format!("message = \"{}\"", msg));
348                }
349                format!("#[validate(range({}))]", parts.join(", "))
350            }
351            SerializableRule::Regex { pattern, message } => {
352                let msg = message
353                    .as_ref()
354                    .map(|m| format!(", message = \"{}\"", m))
355                    .unwrap_or_default();
356                format!("#[validate(regex = \"{}\"{})]", pattern, msg)
357            }
358            SerializableRule::Url { message } => {
359                let msg = message
360                    .as_ref()
361                    .map(|m| format!(", message = \"{}\"", m))
362                    .unwrap_or_default();
363                format!("#[validate(url{})]", msg)
364            }
365            SerializableRule::Required { message } => {
366                let msg = message
367                    .as_ref()
368                    .map(|m| format!(", message = \"{}\"", m))
369                    .unwrap_or_default();
370                format!("#[validate(required{})]", msg)
371            }
372            SerializableRule::AsyncUnique {
373                table,
374                column,
375                message,
376            } => {
377                let msg = message
378                    .as_ref()
379                    .map(|m| format!(", message = \"{}\"", m))
380                    .unwrap_or_default();
381                format!(
382                    "#[validate(async_unique(table = \"{}\", column = \"{}\"{}))]",
383                    table, column, msg
384                )
385            }
386            SerializableRule::AsyncExists {
387                table,
388                column,
389                message,
390            } => {
391                let msg = message
392                    .as_ref()
393                    .map(|m| format!(", message = \"{}\"", m))
394                    .unwrap_or_default();
395                format!(
396                    "#[validate(async_exists(table = \"{}\", column = \"{}\"{}))]",
397                    table, column, msg
398                )
399            }
400            SerializableRule::AsyncApi { endpoint, message } => {
401                let msg = message
402                    .as_ref()
403                    .map(|m| format!(", message = \"{}\"", m))
404                    .unwrap_or_default();
405                format!("#[validate(async_api(endpoint = \"{}\"{}))]", endpoint, msg)
406            }
407            SerializableRule::CreditCard { message } => {
408                let msg = message
409                    .as_ref()
410                    .map(|m| format!(", message = \"{}\"", m))
411                    .unwrap_or_default();
412                format!("#[validate(credit_card{})]", msg)
413            }
414            SerializableRule::Ip { v4, v6, message } => {
415                let mut parts = Vec::new();
416                if let Some(true) = v4 {
417                    parts.push("v4".to_string());
418                }
419                if let Some(true) = v6 {
420                    parts.push("v6".to_string());
421                }
422                if let Some(msg) = message {
423                    parts.push(format!("message = \"{}\"", msg));
424                }
425                if parts.is_empty() {
426                    "#[validate(ip)]".to_string()
427                } else {
428                    format!("#[validate(ip({}))]", parts.join(", "))
429                }
430            }
431            SerializableRule::Phone { message } => {
432                let msg = message
433                    .as_ref()
434                    .map(|m| format!(", message = \"{}\"", m))
435                    .unwrap_or_default();
436                format!("#[validate(phone{})]", msg)
437            }
438            SerializableRule::Contains { needle, message } => {
439                let msg = message
440                    .as_ref()
441                    .map(|m| format!(", message = \"{}\"", m))
442                    .unwrap_or_default();
443                format!("#[validate(contains(needle = \"{}\"{}))]", needle, msg)
444            }
445            SerializableRule::CustomAsync { function, message } => {
446                let msg = message
447                    .as_ref()
448                    .map(|m| format!(", message = \"{}\"", m))
449                    .unwrap_or_default();
450                format!("#[validate(custom_async = \"{}\"{})]", function, msg)
451            }
452        }
453    }
454
455    /// Parse a SerializableRule from a pretty-printed string.
456    ///
457    /// This is the inverse of `pretty_print()` and enables round-trip
458    /// serialization of validation rules.
459    pub fn parse(s: &str) -> Option<Self> {
460        let s = s.trim();
461
462        // Must start with #[validate( and end with )]
463        if !s.starts_with("#[validate(") || !s.ends_with(")]") {
464            return None;
465        }
466
467        // Extract the inner content
468        let inner = &s[11..s.len() - 2];
469
470        // Parse based on rule type
471        if inner == "email" || inner.starts_with("email,") {
472            let message = Self::extract_message(inner);
473            return Some(SerializableRule::Email { message });
474        }
475
476        if inner == "url" || inner.starts_with("url,") {
477            let message = Self::extract_message(inner);
478            return Some(SerializableRule::Url { message });
479        }
480
481        if inner == "required" || inner.starts_with("required,") {
482            let message = Self::extract_message(inner);
483            return Some(SerializableRule::Required { message });
484        }
485
486        if inner.starts_with("length(") {
487            return Self::parse_length(inner);
488        }
489
490        if inner.starts_with("range(") {
491            return Self::parse_range(inner);
492        }
493
494        if inner.starts_with("regex") {
495            return Self::parse_regex(inner);
496        }
497
498        if inner.starts_with("async_unique(") {
499            return Self::parse_async_unique(inner);
500        }
501
502        if inner.starts_with("async_exists(") {
503            return Self::parse_async_exists(inner);
504        }
505
506        if inner.starts_with("async_api(") {
507            return Self::parse_async_api(inner);
508        }
509
510        if inner == "credit_card" || inner.starts_with("credit_card,") {
511            let message = Self::extract_message(inner);
512            return Some(SerializableRule::CreditCard { message });
513        }
514
515        if inner == "ip" {
516            return Some(SerializableRule::Ip {
517                v4: None,
518                v6: None,
519                message: None,
520            });
521        }
522
523        if inner.starts_with("ip(") {
524            return Self::parse_ip(inner);
525        }
526
527        if inner == "phone" || inner.starts_with("phone,") {
528            let message = Self::extract_message(inner);
529            return Some(SerializableRule::Phone { message });
530        }
531
532        if inner.starts_with("contains(") {
533            return Self::parse_contains(inner);
534        }
535
536        if inner.starts_with("custom_async") {
537            return Self::parse_custom_async(inner);
538        }
539
540        None
541    }
542
543    fn extract_message(s: &str) -> Option<String> {
544        if let Some(idx) = s.find("message = \"") {
545            let start = idx + 11;
546            if let Some(end) = s[start..].find('"') {
547                return Some(s[start..start + end].to_string());
548            }
549        }
550        None
551    }
552
553    fn extract_param(s: &str, param: &str) -> Option<String> {
554        let pattern = format!("{} = ", param);
555        if let Some(idx) = s.find(&pattern) {
556            let start = idx + pattern.len();
557            let rest = &s[start..];
558
559            // Check if it's a quoted string
560            if let Some(stripped) = rest.strip_prefix('"') {
561                if let Some(end) = stripped.find('"') {
562                    return Some(stripped[..end].to_string());
563                }
564            } else {
565                // It's a number or other value
566                let end = rest.find([',', ')']).unwrap_or(rest.len());
567                return Some(rest[..end].trim().to_string());
568            }
569        }
570        None
571    }
572
573    fn parse_length(s: &str) -> Option<Self> {
574        let min = Self::extract_param(s, "min").and_then(|v| v.parse().ok());
575        let max = Self::extract_param(s, "max").and_then(|v| v.parse().ok());
576        let message = Self::extract_message(s);
577        Some(SerializableRule::Length { min, max, message })
578    }
579
580    fn parse_range(s: &str) -> Option<Self> {
581        let min = Self::extract_param(s, "min").and_then(|v| v.parse().ok());
582        let max = Self::extract_param(s, "max").and_then(|v| v.parse().ok());
583        let message = Self::extract_message(s);
584        Some(SerializableRule::Range { min, max, message })
585    }
586
587    fn parse_regex(s: &str) -> Option<Self> {
588        let pattern =
589            Self::extract_param(s, "regex").or_else(|| Self::extract_param(s, "pattern"))?;
590        let message = Self::extract_message(s);
591        Some(SerializableRule::Regex { pattern, message })
592    }
593
594    fn parse_async_unique(s: &str) -> Option<Self> {
595        let table = Self::extract_param(s, "table")?;
596        let column = Self::extract_param(s, "column")?;
597        let message = Self::extract_message(s);
598        Some(SerializableRule::AsyncUnique {
599            table,
600            column,
601            message,
602        })
603    }
604
605    fn parse_async_exists(s: &str) -> Option<Self> {
606        let table = Self::extract_param(s, "table")?;
607        let column = Self::extract_param(s, "column")?;
608        let message = Self::extract_message(s);
609        Some(SerializableRule::AsyncExists {
610            table,
611            column,
612            message,
613        })
614    }
615
616    fn parse_async_api(s: &str) -> Option<Self> {
617        let endpoint = Self::extract_param(s, "endpoint")?;
618        let message = Self::extract_message(s);
619        Some(SerializableRule::AsyncApi { endpoint, message })
620    }
621
622    fn parse_ip(s: &str) -> Option<Self> {
623        let v4 = if s.contains("v4") { Some(true) } else { None };
624        let v6 = if s.contains("v6") { Some(true) } else { None };
625        let message = Self::extract_message(s);
626        Some(SerializableRule::Ip { v4, v6, message })
627    }
628
629    fn parse_contains(s: &str) -> Option<Self> {
630        let needle = Self::extract_param(s, "needle")?;
631        let message = Self::extract_message(s);
632        Some(SerializableRule::Contains { needle, message })
633    }
634
635    fn parse_custom_async(s: &str) -> Option<Self> {
636        // Handle both simple 'custom_async = "func"' and logical 'custom_async(function = "func")'
637        let function = Self::extract_param(s, "custom_async")
638            .or_else(|| Self::extract_param(s, "function"))?;
639        let message = Self::extract_message(s);
640        Some(SerializableRule::CustomAsync { function, message })
641    }
642}
643
644// Conversion implementations from concrete rules to SerializableRule
645use crate::v2::rules::{
646    AsyncApiRule, AsyncExistsRule, AsyncUniqueRule, ContainsRule, CreditCardRule, EmailRule,
647    IpRule, LengthRule, PhoneRule, RegexRule, RequiredRule, UrlRule,
648};
649
650impl From<EmailRule> for SerializableRule {
651    fn from(rule: EmailRule) -> Self {
652        SerializableRule::Email {
653            message: rule.message,
654        }
655    }
656}
657
658impl From<LengthRule> for SerializableRule {
659    fn from(rule: LengthRule) -> Self {
660        SerializableRule::Length {
661            min: rule.min,
662            max: rule.max,
663            message: rule.message,
664        }
665    }
666}
667
668impl From<RegexRule> for SerializableRule {
669    fn from(rule: RegexRule) -> Self {
670        SerializableRule::Regex {
671            pattern: rule.pattern,
672            message: rule.message,
673        }
674    }
675}
676
677impl From<UrlRule> for SerializableRule {
678    fn from(rule: UrlRule) -> Self {
679        SerializableRule::Url {
680            message: rule.message,
681        }
682    }
683}
684
685impl From<RequiredRule> for SerializableRule {
686    fn from(rule: RequiredRule) -> Self {
687        SerializableRule::Required {
688            message: rule.message,
689        }
690    }
691}
692
693impl From<AsyncUniqueRule> for SerializableRule {
694    fn from(rule: AsyncUniqueRule) -> Self {
695        SerializableRule::AsyncUnique {
696            table: rule.table,
697            column: rule.column,
698            message: rule.message,
699        }
700    }
701}
702
703impl From<AsyncExistsRule> for SerializableRule {
704    fn from(rule: AsyncExistsRule) -> Self {
705        SerializableRule::AsyncExists {
706            table: rule.table,
707            column: rule.column,
708            message: rule.message,
709        }
710    }
711}
712
713impl From<AsyncApiRule> for SerializableRule {
714    fn from(rule: AsyncApiRule) -> Self {
715        SerializableRule::AsyncApi {
716            endpoint: rule.endpoint,
717            message: rule.message,
718        }
719    }
720}
721
722impl From<CreditCardRule> for SerializableRule {
723    fn from(rule: CreditCardRule) -> Self {
724        SerializableRule::CreditCard {
725            message: rule.message,
726        }
727    }
728}
729
730impl From<IpRule> for SerializableRule {
731    fn from(rule: IpRule) -> Self {
732        SerializableRule::Ip {
733            v4: rule.v4,
734            v6: rule.v6,
735            message: rule.message,
736        }
737    }
738}
739
740impl From<PhoneRule> for SerializableRule {
741    fn from(rule: PhoneRule) -> Self {
742        SerializableRule::Phone {
743            message: rule.message,
744        }
745    }
746}
747
748impl From<ContainsRule> for SerializableRule {
749    fn from(rule: ContainsRule) -> Self {
750        SerializableRule::Contains {
751            needle: rule.needle,
752            message: rule.message,
753        }
754    }
755}
756
757#[cfg(test)]
758mod tests {
759    use super::*;
760
761    #[test]
762    fn serializable_rule_email_pretty_print() {
763        let rule = SerializableRule::Email { message: None };
764        assert_eq!(rule.pretty_print(), "#[validate(email)]");
765
766        let rule = SerializableRule::Email {
767            message: Some("Invalid email".to_string()),
768        };
769        assert_eq!(
770            rule.pretty_print(),
771            "#[validate(email, message = \"Invalid email\")]"
772        );
773    }
774
775    #[test]
776    fn serializable_rule_length_pretty_print() {
777        let rule = SerializableRule::Length {
778            min: Some(3),
779            max: Some(50),
780            message: None,
781        };
782        assert_eq!(
783            rule.pretty_print(),
784            "#[validate(length(min = 3, max = 50))]"
785        );
786    }
787
788    #[test]
789    fn serializable_rule_roundtrip() {
790        let rule = SerializableRule::Range {
791            min: Some(18.0),
792            max: Some(120.0),
793            message: Some("Age must be between 18 and 120".to_string()),
794        };
795
796        let json = serde_json::to_string(&rule).unwrap();
797        let parsed: SerializableRule = serde_json::from_str(&json).unwrap();
798        assert_eq!(rule, parsed);
799    }
800
801    #[test]
802    fn serializable_rule_pretty_print_roundtrip_email() {
803        let rule = SerializableRule::Email { message: None };
804        let pretty = rule.pretty_print();
805        let parsed = SerializableRule::parse(&pretty).unwrap();
806        assert_eq!(rule, parsed);
807
808        let rule = SerializableRule::Email {
809            message: Some("Invalid email".to_string()),
810        };
811        let pretty = rule.pretty_print();
812        let parsed = SerializableRule::parse(&pretty).unwrap();
813        assert_eq!(rule, parsed);
814    }
815
816    #[test]
817    fn serializable_rule_pretty_print_roundtrip_length() {
818        let rule = SerializableRule::Length {
819            min: Some(3),
820            max: Some(50),
821            message: None,
822        };
823        let pretty = rule.pretty_print();
824        let parsed = SerializableRule::parse(&pretty).unwrap();
825        assert_eq!(rule, parsed);
826    }
827
828    #[test]
829    fn serializable_rule_pretty_print_roundtrip_range() {
830        let rule = SerializableRule::Range {
831            min: Some(18.0),
832            max: Some(120.0),
833            message: None,
834        };
835        let pretty = rule.pretty_print();
836        let parsed = SerializableRule::parse(&pretty).unwrap();
837        assert_eq!(rule, parsed);
838    }
839
840    #[test]
841    fn serializable_rule_pretty_print_roundtrip_url() {
842        let rule = SerializableRule::Url { message: None };
843        let pretty = rule.pretty_print();
844        let parsed = SerializableRule::parse(&pretty).unwrap();
845        assert_eq!(rule, parsed);
846    }
847
848    #[test]
849    fn serializable_rule_pretty_print_roundtrip_required() {
850        let rule = SerializableRule::Required { message: None };
851        let pretty = rule.pretty_print();
852        let parsed = SerializableRule::parse(&pretty).unwrap();
853        assert_eq!(rule, parsed);
854    }
855
856    #[test]
857    fn serializable_rule_pretty_print_roundtrip_async_unique() {
858        let rule = SerializableRule::AsyncUnique {
859            table: "users".to_string(),
860            column: "email".to_string(),
861            message: None,
862        };
863        let pretty = rule.pretty_print();
864        let parsed = SerializableRule::parse(&pretty).unwrap();
865        assert_eq!(rule, parsed);
866    }
867
868    #[test]
869    fn serializable_rule_pretty_print_roundtrip_async_exists() {
870        let rule = SerializableRule::AsyncExists {
871            table: "categories".to_string(),
872            column: "id".to_string(),
873            message: Some("Category not found".to_string()),
874        };
875        let pretty = rule.pretty_print();
876        let parsed = SerializableRule::parse(&pretty).unwrap();
877        assert_eq!(rule, parsed);
878    }
879
880    #[test]
881    fn serializable_rule_pretty_print_roundtrip_async_api() {
882        let rule = SerializableRule::AsyncApi {
883            endpoint: "https://api.example.com/validate".to_string(),
884            message: None,
885        };
886        let pretty = rule.pretty_print();
887        let parsed = SerializableRule::parse(&pretty).unwrap();
888        assert_eq!(rule, parsed);
889    }
890
891    #[test]
892    fn from_email_rule() {
893        let rule = EmailRule::new().with_message("Invalid email");
894        let serializable: SerializableRule = rule.into();
895        assert_eq!(
896            serializable,
897            SerializableRule::Email {
898                message: Some("Invalid email".to_string())
899            }
900        );
901    }
902
903    #[test]
904    fn from_length_rule() {
905        let rule = LengthRule::new(3, 50).with_message("Invalid length");
906        let serializable: SerializableRule = rule.into();
907        assert_eq!(
908            serializable,
909            SerializableRule::Length {
910                min: Some(3),
911                max: Some(50),
912                message: Some("Invalid length".to_string())
913            }
914        );
915    }
916
917    #[test]
918    fn from_async_unique_rule() {
919        let rule = AsyncUniqueRule::new("users", "email").with_message("Email taken");
920        let serializable: SerializableRule = rule.into();
921        assert_eq!(
922            serializable,
923            SerializableRule::AsyncUnique {
924                table: "users".to_string(),
925                column: "email".to_string(),
926                message: Some("Email taken".to_string())
927            }
928        );
929    }
930}