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 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 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 if !config.flat_mode && !schema.nested_types.is_empty() {
151 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 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 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 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}