mold_cli/generators/
typescript.rs

1use crate::generators::{Generator, GeneratorConfig};
2use crate::types::{NestedType, ObjectType, Schema, SchemaType};
3use crate::utils::{is_ts_reserved, sanitize_identifier};
4use anyhow::Result;
5use std::collections::HashMap;
6
7pub struct TypeScriptGenerator;
8
9impl TypeScriptGenerator {
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 => "string".to_string(),
22            SchemaType::Number | SchemaType::Integer => "number".to_string(),
23            SchemaType::Boolean => "boolean".to_string(),
24            SchemaType::Null => "null".to_string(),
25            SchemaType::Any => "unknown".to_string(),
26            SchemaType::Array(inner) => {
27                let inner_type = self.generate_type(inner, indent, type_refs);
28                if matches!(**inner, SchemaType::Union(_)) {
29                    format!("({})[]", inner_type)
30                } else {
31                    format!("{}[]", inner_type)
32                }
33            }
34            SchemaType::Optional(inner) => {
35                let inner_type = self.generate_type(inner, indent, type_refs);
36                format!("{} | undefined", inner_type)
37            }
38            SchemaType::Union(types) => {
39                let type_strings: Vec<String> = types
40                    .iter()
41                    .map(|t| self.generate_type(t, indent, type_refs))
42                    .collect();
43                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                    type_name.clone()
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 "Record<string, unknown>".to_string();
65        }
66
67        let mut lines = vec!["{".to_string()];
68        for field in &obj.fields {
69            let field_name = self.format_field_name(&field.name);
70            let field_type =
71                self.generate_type(&field.field_type, &format!("{}  ", indent), type_refs);
72            let optional = if field.optional { "?" } else { "" };
73            lines.push(format!(
74                "{}  {}{}: {};",
75                indent, field_name, optional, field_type
76            ));
77        }
78        lines.push(format!("{}}}", indent));
79        lines.join("\n")
80    }
81
82    fn generate_interface(
83        &self,
84        name: &str,
85        obj: &ObjectType,
86        indent: &str,
87        type_refs: &HashMap<String, String>,
88    ) -> String {
89        let mut lines = vec![format!("interface {} {{", name)];
90
91        for field in &obj.fields {
92            let field_name = self.format_field_name(&field.name);
93            let field_type = self.generate_type(&field.field_type, indent, type_refs);
94            let optional = if field.optional { "?" } else { "" };
95            lines.push(format!("{}{}{}: {};", indent, field_name, optional, field_type));
96        }
97
98        lines.push("}".to_string());
99        lines.join("\n")
100    }
101
102    fn format_field_name(&self, name: &str) -> String {
103        let sanitized = sanitize_identifier(name);
104        if name != sanitized || is_ts_reserved(name) || name.contains('-') || name.contains(' ') {
105            format!("\"{}\"", name)
106        } else {
107            name.to_string()
108        }
109    }
110
111    fn build_type_refs(&self, nested_types: &[NestedType]) -> HashMap<String, String> {
112        let mut refs = HashMap::new();
113        for nt in nested_types {
114            let key = format!("{:?}", nt.object);
115            refs.insert(key, nt.name.clone());
116        }
117        refs
118    }
119}
120
121impl Default for TypeScriptGenerator {
122    fn default() -> Self {
123        Self::new()
124    }
125}
126
127impl Generator for TypeScriptGenerator {
128    fn generate(&self, schema: &Schema, config: &GeneratorConfig) -> Result<String> {
129        let mut output = vec!["// Generated by mold".to_string(), String::new()];
130
131        // Build type reference map
132        let type_refs = if config.flat_mode {
133            HashMap::new()
134        } else {
135            self.build_type_refs(&schema.nested_types)
136        };
137
138        // Generate nested types first (if not flat mode)
139        if !config.flat_mode && !schema.nested_types.is_empty() {
140            // Output in reverse order (deepest nested first)
141            for nt in schema.nested_types.iter().rev() {
142                output.push(self.generate_interface(&nt.name, &nt.object, &config.indent, &type_refs));
143                output.push(String::new());
144            }
145        }
146
147        // Generate root interface
148        if let SchemaType::Object(obj) = &schema.root_type {
149            output.push(self.generate_interface(&schema.name, obj, &config.indent, &type_refs));
150        }
151
152        Ok(output.join("\n"))
153    }
154
155    fn file_extension(&self) -> &'static str {
156        "ts"
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use crate::types::Field;
164
165    #[test]
166    fn test_generate_simple_interface() {
167        let gen = TypeScriptGenerator::new();
168        let obj = ObjectType::new(vec![
169            Field::new("id", SchemaType::Integer),
170            Field::new("name", SchemaType::String),
171            Field::new("active", SchemaType::Boolean),
172        ]);
173        let schema = Schema::new("User", SchemaType::Object(obj));
174        let config = GeneratorConfig::default();
175
176        let output = gen.generate(&schema, &config).unwrap();
177
178        assert!(output.contains("interface User"));
179        assert!(output.contains("id: number"));
180        assert!(output.contains("name: string"));
181        assert!(output.contains("active: boolean"));
182    }
183
184    #[test]
185    fn test_generate_array_type() {
186        let gen = TypeScriptGenerator::new();
187        let obj = ObjectType::new(vec![Field::new(
188            "tags",
189            SchemaType::Array(Box::new(SchemaType::String)),
190        )]);
191        let schema = Schema::new("Test", SchemaType::Object(obj));
192        let config = GeneratorConfig::default();
193
194        let output = gen.generate(&schema, &config).unwrap();
195
196        assert!(output.contains("tags: string[]"));
197    }
198
199    #[test]
200    fn test_generate_union_type() {
201        let gen = TypeScriptGenerator::new();
202        let obj = ObjectType::new(vec![Field::new(
203            "value",
204            SchemaType::Union(vec![SchemaType::String, SchemaType::Integer]),
205        )]);
206        let schema = Schema::new("Test", SchemaType::Object(obj));
207        let config = GeneratorConfig::default();
208
209        let output = gen.generate(&schema, &config).unwrap();
210
211        assert!(output.contains("value: string | number"));
212    }
213
214    #[test]
215    fn test_empty_object() {
216        let gen = TypeScriptGenerator::new();
217        let obj = ObjectType::new(vec![Field::new(
218            "metadata",
219            SchemaType::Object(ObjectType::empty()),
220        )]);
221        let schema = Schema::new("Test", SchemaType::Object(obj));
222        let config = GeneratorConfig::default();
223
224        let output = gen.generate(&schema, &config).unwrap();
225
226        assert!(output.contains("Record<string, unknown>"));
227    }
228}