gear_mesh_core/
validation.rs

1//! バリデーションルール定義
2//!
3//! Rustのバリデーション属性から抽出されるルールを表現します。
4
5use serde::{Deserialize, Serialize};
6
7/// バリデーションルール
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub enum ValidationRule {
10    /// 範囲チェック
11    Range { min: Option<f64>, max: Option<f64> },
12    /// 文字列長さチェック
13    Length {
14        min: Option<usize>,
15        max: Option<usize>,
16    },
17    /// 正規表現パターン
18    Pattern(String),
19    /// メールアドレス形式
20    Email,
21    /// URL形式
22    Url,
23    /// 必須フィールド
24    Required,
25    /// カスタムバリデーション
26    Custom {
27        name: String,
28        message: Option<String>,
29    },
30}
31
32impl ValidationRule {
33    /// TypeScriptのバリデーションコードを生成
34    pub fn to_typescript_check(&self, field_name: &str) -> String {
35        match self {
36            ValidationRule::Range { min, max } => {
37                let mut checks = Vec::new();
38                if let Some(min) = min {
39                    checks.push(format!("obj.{field_name} >= {min}"));
40                }
41                if let Some(max) = max {
42                    checks.push(format!("obj.{field_name} <= {max}"));
43                }
44                if checks.is_empty() {
45                    "true".to_string()
46                } else {
47                    checks.join(" && ")
48                }
49            }
50            ValidationRule::Length { min, max } => {
51                let mut checks = Vec::new();
52                if let Some(min) = min {
53                    checks.push(format!("obj.{field_name}.length >= {min}"));
54                }
55                if let Some(max) = max {
56                    checks.push(format!("obj.{field_name}.length <= {max}"));
57                }
58                if checks.is_empty() {
59                    "true".to_string()
60                } else {
61                    checks.join(" && ")
62                }
63            }
64            ValidationRule::Pattern(pattern) => {
65                format!("/{pattern}/.test(obj.{field_name})")
66            }
67            ValidationRule::Email => {
68                format!(r#"/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(obj.{field_name})"#)
69            }
70            ValidationRule::Url => {
71                format!(r#"/^https?:\/\/[^\s]+$/.test(obj.{field_name})"#)
72            }
73            ValidationRule::Required => {
74                format!("obj.{field_name} !== undefined && obj.{field_name} !== null")
75            }
76            ValidationRule::Custom { name, .. } => {
77                format!("validate{name}(obj.{field_name})")
78            }
79        }
80    }
81
82    /// Zodスキーマコードを生成
83    pub fn to_zod_schema(&self, is_bigint: bool) -> String {
84        match self {
85            ValidationRule::Range { min, max } => {
86                let mut schema = String::new();
87                let suffix = if is_bigint { "n" } else { "" };
88                if let Some(min) = min {
89                    if is_bigint {
90                        debug_assert!(
91                            min.fract() == 0.0,
92                            "Fractional value ({}) provided for integer range validation, this will be truncated to {}",
93                            min,
94                            *min as i128
95                        );
96                        schema.push_str(&format!(".min({}{suffix})", *min as i128));
97                    } else {
98                        schema.push_str(&format!(".min({min})"));
99                    }
100                }
101                if let Some(max) = max {
102                    if is_bigint {
103                        debug_assert!(
104                            max.fract() == 0.0,
105                            "Fractional value ({}) provided for integer range validation, this will be truncated to {}",
106                            max,
107                            *max as i128
108                        );
109                        schema.push_str(&format!(".max({}{suffix})", *max as i128));
110                    } else {
111                        schema.push_str(&format!(".max({max})"));
112                    }
113                }
114                schema
115            }
116            ValidationRule::Length { min, max } => {
117                let mut schema = String::new();
118                if let Some(min) = min {
119                    schema.push_str(&format!(".min({min})"));
120                }
121                if let Some(max) = max {
122                    schema.push_str(&format!(".max({max})"));
123                }
124                schema
125            }
126            ValidationRule::Pattern(pattern) => {
127                format!(".regex(/{pattern}/)")
128            }
129            ValidationRule::Email => ".email()".to_string(),
130            ValidationRule::Url => ".url()".to_string(),
131            ValidationRule::Required => String::new(), // Zodではデフォルトで必須
132            ValidationRule::Custom { name, message } => {
133                if let Some(msg) = message {
134                    format!(".refine(validate{name}, {{ message: \"{msg}\" }})")
135                } else {
136                    format!(".refine(validate{name})")
137                }
138            }
139        }
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_range_validation() {
149        let rule = ValidationRule::Range {
150            min: Some(1.0),
151            max: Some(100.0),
152        };
153        assert_eq!(
154            rule.to_typescript_check("age"),
155            "obj.age >= 1 && obj.age <= 100"
156        );
157    }
158
159    #[test]
160    fn test_email_validation() {
161        let rule = ValidationRule::Email;
162        assert!(rule.to_typescript_check("email").contains("@"));
163    }
164}