gear_mesh_generator/
validation_gen.rs

1use crate::GeneratorConfig;
2use gear_mesh_core::{FieldInfo, GearMeshType, TypeKind};
3
4/// Generator for Zod validation schemas
5pub struct ValidationGenerator {
6    config: GeneratorConfig,
7}
8
9impl ValidationGenerator {
10    pub fn new(config: GeneratorConfig) -> Self {
11        Self { config }
12    }
13
14    /// Generates a Zod schema
15    pub fn generate_zod_schema(&self, ty: &GearMeshType) -> Option<String> {
16        match &ty.kind {
17            TypeKind::Struct(s) => {
18                let mut schema = format!("export const {}Schema = z.object({{\n", ty.name);
19
20                for field in &s.fields {
21                    let field_schema = self.field_to_zod(field);
22                    schema.push_str(&format!("    {}: {},\n", field.name, field_schema));
23                }
24
25                schema.push_str("});\n");
26                Some(schema)
27            }
28            _ => None,
29        }
30    }
31
32    fn field_to_zod(&self, field: &FieldInfo) -> String {
33        let is_option = field.optional;
34
35        // Extract the target type for validation and schema generation.
36        // For Option<T> fields, we need to unwrap to get T here because:
37        // 1. Validation rules apply to the inner type T, not the Option wrapper
38        // 2. We need to append .nullable() AFTER validation rules (e.g., .min().max().nullable())
39        // 3. The recursive type_to_zod handles nested Options in complex types (e.g., Vec<Option<String>>)
40        let target_type = if field.ty.name == "Option" && !field.ty.generics.is_empty() {
41            &field.ty.generics[0]
42        } else {
43            &field.ty
44        };
45
46        // ベースとなるスキーマを生成
47        let base_schema = self.type_to_zod(target_type);
48
49        // バリデーションルールの適用のための型判定(BigIntかどうか)
50        // NOTE: ここでの判定は最上位の型に対してのみ有効
51        let is_bigint = self.config.use_bigint && crate::utils::is_bigint_type(&target_type.name);
52
53        let mut result = base_schema;
54
55        // IMPORTANT: Add validation rules BEFORE nullable
56        for rule in &field.validations {
57            result.push_str(&rule.to_zod_schema(is_bigint));
58        }
59
60        // Add nullable for Option types AFTER validations
61        if is_option {
62            result.push_str(".nullable()");
63        }
64
65        result
66    }
67
68    /// Recursively generates a Zod schema from a TypeRef
69    fn type_to_zod(&self, type_ref: &gear_mesh_core::TypeRef) -> String {
70        match type_ref.name.as_str() {
71            // プリミティブ型
72            name if crate::utils::is_builtin_type(name) => {
73                // コレクション型は個別に処理
74                match name {
75                    "Vec" | "Array" => {
76                        if !type_ref.generics.is_empty() {
77                            let inner_schema = self.type_to_zod(&type_ref.generics[0]);
78                            format!("z.array({})", inner_schema)
79                        } else {
80                            "z.array(z.unknown())".to_string()
81                        }
82                    }
83                    "Option" => {
84                        if !type_ref.generics.is_empty() {
85                            let inner_schema = self.type_to_zod(&type_ref.generics[0]);
86                            // Avoid generating a double-nullable schema like `z.string().nullable().nullable()`
87                            if inner_schema.ends_with(".nullable()") {
88                                inner_schema
89                            } else {
90                                format!("{}.nullable()", inner_schema)
91                            }
92                        } else {
93                            "z.unknown().nullable()".to_string()
94                        }
95                    }
96                    "HashMap" | "BTreeMap" => {
97                        let value_schema = if type_ref.generics.len() >= 2 {
98                            self.type_to_zod(&type_ref.generics[1])
99                        } else {
100                            "z.unknown()".to_string()
101                        };
102                        // HashMap<K, V> -> z.record(V) (Key is always string in JS objects usually, but Zod supports record)
103                        format!("z.record({})", value_schema)
104                    }
105                    "HashSet" | "BTreeSet" => {
106                        if !type_ref.generics.is_empty() {
107                            format!("z.set({})", self.type_to_zod(&type_ref.generics[0]))
108                        } else {
109                            "z.set(z.unknown())".to_string()
110                        }
111                    }
112                    _ => self.get_zod_primitive_type(name),
113                }
114            }
115            // カスタム型
116            name => format!("{}Schema", name),
117        }
118    }
119
120    fn get_zod_primitive_type(&self, type_name: &str) -> String {
121        match type_name {
122            "i8" | "i16" | "i32" | "u8" | "u16" | "u32" | "f32" | "f64" => "z.number()".to_string(),
123            "i64" | "i128" | "u64" | "u128" | "isize" | "usize" => {
124                if self.config.use_bigint {
125                    "z.bigint()".to_string()
126                } else {
127                    "z.number()".to_string()
128                }
129            }
130            "String" | "str" | "char" => "z.string()".to_string(),
131            "bool" => "z.boolean()".to_string(),
132            _ => "z.unknown()".to_string(),
133        }
134    }
135}
136
137impl Default for ValidationGenerator {
138    fn default() -> Self {
139        Self::new(GeneratorConfig::default())
140    }
141}