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 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 let type_refs = if config.flat_mode {
133 HashMap::new()
134 } else {
135 self.build_type_refs(&schema.nested_types)
136 };
137
138 if !config.flat_mode && !schema.nested_types.is_empty() {
140 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 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}