mold_cli/generators/
zod.rs

1use crate::generators::{Generator, GeneratorConfig};
2use crate::types::{NestedType, ObjectType, Schema, SchemaType};
3use crate::utils::sanitize_identifier;
4use anyhow::Result;
5use std::collections::HashMap;
6
7pub struct ZodGenerator;
8
9impl ZodGenerator {
10    pub fn new() -> Self {
11        Self
12    }
13
14    fn generate_type(
15        &self,
16        schema_type: &SchemaType,
17        indent: &str,
18        type_refs: &HashMap<String, String>,
19    ) -> String {
20        match schema_type {
21            SchemaType::String => "z.string()".to_string(),
22            SchemaType::Number => "z.number()".to_string(),
23            SchemaType::Integer => "z.number().int()".to_string(),
24            SchemaType::Boolean => "z.boolean()".to_string(),
25            SchemaType::Null => "z.null()".to_string(),
26            SchemaType::Any => "z.unknown()".to_string(),
27            SchemaType::Array(inner) => {
28                let inner_type = self.generate_type(inner, indent, type_refs);
29                format!("z.array({})", inner_type)
30            }
31            SchemaType::Optional(inner) => {
32                let inner_type = self.generate_type(inner, indent, type_refs);
33                format!("{}.optional()", inner_type)
34            }
35            SchemaType::Union(types) => {
36                if types.len() == 1 {
37                    return self.generate_type(&types[0], indent, type_refs);
38                }
39                let type_strings: Vec<String> = types
40                    .iter()
41                    .map(|t| self.generate_type(t, indent, type_refs))
42                    .collect();
43                format!("z.union([{}])", type_strings.join(", "))
44            }
45            SchemaType::Object(obj) => {
46                // Check if this object has a type reference
47                let obj_key = format!("{:?}", obj);
48                if let Some(type_name) = type_refs.get(&obj_key) {
49                    format!("{}Schema", type_name)
50                } else {
51                    self.generate_inline_object(obj, indent, type_refs)
52                }
53            }
54        }
55    }
56
57    fn generate_inline_object(
58        &self,
59        obj: &ObjectType,
60        indent: &str,
61        type_refs: &HashMap<String, String>,
62    ) -> String {
63        if obj.fields.is_empty() {
64            return "z.record(z.unknown())".to_string();
65        }
66
67        let inner_indent = format!("{}  ", indent);
68        let mut lines = vec!["z.object({".to_string()];
69
70        for field in &obj.fields {
71            let field_name = self.format_field_name(&field.name);
72            let mut field_type = self.generate_type(&field.field_type, &inner_indent, type_refs);
73            if field.optional {
74                field_type = format!("{}.optional()", field_type);
75            }
76            lines.push(format!("{}{}: {},", inner_indent, field_name, field_type));
77        }
78
79        lines.push(format!("{}}})", indent));
80        lines.join("\n")
81    }
82
83    fn generate_schema(
84        &self,
85        name: &str,
86        obj: &ObjectType,
87        indent: &str,
88        type_refs: &HashMap<String, String>,
89    ) -> String {
90        let schema_name = format!("{}Schema", name);
91        let inner_indent = format!("{}  ", indent);
92        let mut lines = vec![format!("const {} = z.object({{", schema_name)];
93
94        for field in &obj.fields {
95            let field_name = self.format_field_name(&field.name);
96            let mut field_type = self.generate_type(&field.field_type, &inner_indent, type_refs);
97            if field.optional {
98                field_type = format!("{}.optional()", field_type);
99            }
100            lines.push(format!("{}{}: {},", inner_indent, field_name, field_type));
101        }
102
103        lines.push("});".to_string());
104        lines.join("\n")
105    }
106
107    fn format_field_name(&self, name: &str) -> String {
108        let sanitized = sanitize_identifier(name);
109        if name != sanitized || name.contains('-') || name.contains(' ') {
110            format!("\"{}\"", name)
111        } else {
112            name.to_string()
113        }
114    }
115
116    fn build_type_refs(&self, nested_types: &[NestedType]) -> HashMap<String, String> {
117        let mut refs = HashMap::new();
118        for nt in nested_types {
119            let key = format!("{:?}", nt.object);
120            refs.insert(key, nt.name.clone());
121        }
122        refs
123    }
124}
125
126impl Default for ZodGenerator {
127    fn default() -> Self {
128        Self::new()
129    }
130}
131
132impl Generator for ZodGenerator {
133    fn generate(&self, schema: &Schema, config: &GeneratorConfig) -> Result<String> {
134        let mut output = vec![
135            "// Generated by mold".to_string(),
136            "import { z } from \"zod\";".to_string(),
137            String::new(),
138        ];
139
140        // Build type reference map
141        let type_refs = if config.flat_mode {
142            HashMap::new()
143        } else {
144            self.build_type_refs(&schema.nested_types)
145        };
146
147        let mut all_type_names: Vec<String> = Vec::new();
148
149        // Generate nested schemas first (if not flat mode)
150        if !config.flat_mode && !schema.nested_types.is_empty() {
151            // Output in reverse order (deepest nested first)
152            for nt in schema.nested_types.iter().rev() {
153                output.push(self.generate_schema(&nt.name, &nt.object, &config.indent, &type_refs));
154                output.push(String::new());
155                all_type_names.push(nt.name.clone());
156            }
157        }
158
159        // Generate root schema
160        if let SchemaType::Object(obj) = &schema.root_type {
161            output.push(self.generate_schema(&schema.name, obj, &config.indent, &type_refs));
162            all_type_names.push(schema.name.clone());
163        }
164
165        output.push(String::new());
166
167        // Generate type aliases
168        for name in &all_type_names {
169            output.push(format!(
170                "type {} = z.infer<typeof {}Schema>;",
171                name, name
172            ));
173        }
174
175        output.push(String::new());
176
177        // Generate exports
178        let schema_exports: Vec<String> =
179            all_type_names.iter().map(|n| format!("{}Schema", n)).collect();
180        output.push(format!("export {{ {} }};", schema_exports.join(", ")));
181        output.push(format!("export type {{ {} }};", all_type_names.join(", ")));
182
183        Ok(output.join("\n"))
184    }
185
186    fn file_extension(&self) -> &'static str {
187        "zod.ts"
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use crate::types::Field;
195
196    #[test]
197    fn test_generate_simple_schema() {
198        let gen = ZodGenerator::new();
199        let obj = ObjectType::new(vec![
200            Field::new("id", SchemaType::Integer),
201            Field::new("name", SchemaType::String),
202            Field::new("active", SchemaType::Boolean),
203        ]);
204        let schema = Schema::new("User", SchemaType::Object(obj));
205        let config = GeneratorConfig::default();
206
207        let output = gen.generate(&schema, &config).unwrap();
208
209        assert!(output.contains("import { z } from \"zod\""));
210        assert!(output.contains("const UserSchema = z.object({"));
211        assert!(output.contains("id: z.number().int()"));
212        assert!(output.contains("name: z.string()"));
213        assert!(output.contains("active: z.boolean()"));
214        assert!(output.contains("type User = z.infer<typeof UserSchema>"));
215        assert!(output.contains("export { UserSchema }"));
216    }
217
218    #[test]
219    fn test_generate_array_schema() {
220        let gen = ZodGenerator::new();
221        let obj = ObjectType::new(vec![Field::new(
222            "tags",
223            SchemaType::Array(Box::new(SchemaType::String)),
224        )]);
225        let schema = Schema::new("Test", SchemaType::Object(obj));
226        let config = GeneratorConfig::default();
227
228        let output = gen.generate(&schema, &config).unwrap();
229
230        assert!(output.contains("tags: z.array(z.string())"));
231    }
232
233    #[test]
234    fn test_generate_union_schema() {
235        let gen = ZodGenerator::new();
236        let obj = ObjectType::new(vec![Field::new(
237            "value",
238            SchemaType::Union(vec![SchemaType::String, SchemaType::Integer]),
239        )]);
240        let schema = Schema::new("Test", SchemaType::Object(obj));
241        let config = GeneratorConfig::default();
242
243        let output = gen.generate(&schema, &config).unwrap();
244
245        assert!(output.contains("z.union([z.string(), z.number().int()])"));
246    }
247
248    #[test]
249    fn test_empty_object() {
250        let gen = ZodGenerator::new();
251        let obj = ObjectType::new(vec![Field::new(
252            "metadata",
253            SchemaType::Object(ObjectType::empty()),
254        )]);
255        let schema = Schema::new("Test", SchemaType::Object(obj));
256        let config = GeneratorConfig::default();
257
258        let output = gen.generate(&schema, &config).unwrap();
259
260        assert!(output.contains("z.record(z.unknown())"));
261    }
262}