baobao_codegen_typescript/
renderer.rs

1//! TypeScript-specific renderer for language-agnostic expressions.
2//!
3//! This module implements the [`Renderer`] trait for TypeScript, translating
4//! [`Value`], [`BuilderSpec`], and [`Block`] into valid TypeScript syntax.
5
6use baobao_codegen::builder::{
7    Binding, Block, BuilderSpec, Constructor, RenderOptions, Renderer, Terminal, Value,
8};
9use baobao_core::to_camel_case;
10
11/// TypeScript language renderer.
12///
13/// Renders language-agnostic expressions to valid TypeScript syntax.
14#[derive(Debug, Clone, Copy, Default)]
15pub struct TypeScriptRenderer;
16
17impl TypeScriptRenderer {
18    /// Create a new TypeScript renderer.
19    pub fn new() -> Self {
20        Self
21    }
22
23    /// Render a binding to TypeScript syntax.
24    fn render_binding(&self, binding: &Binding, opts: &RenderOptions) -> String {
25        let indent = opts.indent_str();
26        let keyword = if binding.mutable { "let" } else { "const" };
27        let value = self.render_value(&binding.value, &opts.nested());
28        format!("{}{} {} = {};", indent, keyword, binding.name, value)
29    }
30}
31
32impl Renderer for TypeScriptRenderer {
33    fn render_value(&self, value: &Value, opts: &RenderOptions) -> String {
34        match value {
35            Value::Bool(v) => v.to_string(),
36            Value::Int(v) => v.to_string(),
37            Value::UInt(v) => v.to_string(),
38            // JavaScript floats don't need special handling
39            Value::Float(v) => v.to_string(),
40            Value::String(v) => format!("\"{}\"", v),
41            Value::Ident(v) => v.clone(),
42            Value::Duration { millis } => {
43                // TypeScript uses milliseconds directly as numbers
44                millis.to_string()
45            }
46            Value::EnumVariant { path, variant } => {
47                // TypeScript enums: EnumName.Variant
48                format!("{}.{}", path, variant)
49            }
50            Value::EnvVar { name, .. } => {
51                // TypeScript/Bun: process.env.NAME or Bun.env.NAME
52                format!("process.env.{}", name)
53            }
54            Value::Try(inner) => {
55                // TypeScript doesn't have ? operator, just render the value
56                // (error handling is done differently, often with try/catch)
57                self.render_value(inner, opts)
58            }
59            Value::Builder(spec) => self.render_builder(spec, opts),
60            Value::Block(block) => self.render_block(block, opts),
61        }
62    }
63
64    fn render_builder(&self, spec: &BuilderSpec, opts: &RenderOptions) -> String {
65        let mut result = self.render_constructor(&spec.constructor);
66
67        if spec.calls.is_empty() {
68            result.push_str(&self.render_terminal(&spec.terminal));
69            return result;
70        }
71
72        if opts.inline {
73            // Single line format
74            for call in &spec.calls {
75                let name = self.transform_method_name(&call.name);
76                if call.args.is_empty() {
77                    result.push_str(&format!(".{}()", name));
78                } else {
79                    let args: Vec<String> = call
80                        .args
81                        .iter()
82                        .map(|a| self.render_value(a, opts))
83                        .collect();
84                    result.push_str(&format!(".{}({})", name, args.join(", ")));
85                }
86            }
87        } else {
88            // Multi-line format
89            let continuation = opts.nested();
90            let indent = continuation.indent_str();
91            for call in &spec.calls {
92                let name = self.transform_method_name(&call.name);
93                if call.args.is_empty() {
94                    result.push_str(&format!("\n{}.{}()", indent, name));
95                } else {
96                    let args: Vec<String> = call
97                        .args
98                        .iter()
99                        .map(|a| self.render_value(a, &continuation))
100                        .collect();
101                    result.push_str(&format!("\n{}.{}({})", indent, name, args.join(", ")));
102                }
103            }
104        }
105
106        result.push_str(&self.render_terminal(&spec.terminal));
107        result
108    }
109
110    fn render_block(&self, block: &Block, opts: &RenderOptions) -> String {
111        if block.bindings.is_empty() {
112            // No bindings, just render the body
113            return self.render_value(&block.body, opts);
114        }
115
116        let indent = opts.indent_str();
117        let inner_opts = opts.nested();
118        let inner_indent = inner_opts.indent_str();
119
120        // TypeScript uses IIFE for block expressions
121        let mut result = String::from("(() => {\n");
122
123        // Render bindings
124        for binding in &block.bindings {
125            result.push_str(&self.render_binding(binding, &inner_opts));
126            result.push('\n');
127        }
128
129        // Render body with return
130        result.push_str(&inner_indent);
131        result.push_str("return ");
132        result.push_str(&self.render_value(&block.body, &inner_opts));
133        result.push_str(";\n");
134
135        result.push_str(&indent);
136        result.push_str("})()");
137
138        result
139    }
140
141    fn transform_method_name(&self, name: &str) -> String {
142        // TypeScript uses camelCase for method names
143        to_camel_case(name)
144    }
145
146    fn render_constructor(&self, ctor: &Constructor) -> String {
147        match ctor {
148            Constructor::StaticNew { type_path } => {
149                // TypeScript uses `new TypeName()`
150                format!("new {}()", type_path)
151            }
152            Constructor::StaticMethod {
153                type_path,
154                method,
155                args,
156            } => {
157                let opts = RenderOptions::inline();
158                let rendered_args: Vec<String> =
159                    args.iter().map(|a| self.render_value(a, &opts)).collect();
160                let method_name = to_camel_case(method);
161                format!(
162                    "{}.{}({})",
163                    type_path,
164                    method_name,
165                    rendered_args.join(", ")
166                )
167            }
168            Constructor::ClassNew { type_name } => {
169                format!("new {}()", type_name)
170            }
171            Constructor::Factory { name } => {
172                format!("{}()", name)
173            }
174        }
175    }
176
177    fn render_terminal(&self, terminal: &Terminal) -> String {
178        let mut result = String::new();
179
180        if let Some(method) = &terminal.method {
181            let method_name = to_camel_case(method);
182            result.push_str(&format!(".{}()", method_name));
183        }
184
185        if terminal.is_async {
186            // In TypeScript, await comes before the expression
187            // But since we're appending, we handle this in the caller
188            // For now, we'll note that this needs await
189        }
190
191        // TypeScript doesn't have ? operator, errors handled differently
192
193        result
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_render_values() {
203        let r = TypeScriptRenderer;
204        let opts = RenderOptions::inline();
205
206        assert_eq!(r.render_value(&Value::Bool(true), &opts), "true");
207        assert_eq!(r.render_value(&Value::Int(-42), &opts), "-42");
208        assert_eq!(r.render_value(&Value::UInt(100), &opts), "100");
209        assert_eq!(
210            r.render_value(&Value::String("hello".into()), &opts),
211            "\"hello\""
212        );
213        assert_eq!(r.render_value(&Value::Ident("foo".into()), &opts), "foo");
214    }
215
216    #[test]
217    fn test_render_duration() {
218        let r = TypeScriptRenderer;
219        let opts = RenderOptions::inline();
220
221        // TypeScript uses raw milliseconds
222        assert_eq!(
223            r.render_value(&Value::Duration { millis: 5000 }, &opts),
224            "5000"
225        );
226        assert_eq!(
227            r.render_value(&Value::Duration { millis: 100 }, &opts),
228            "100"
229        );
230    }
231
232    #[test]
233    fn test_render_env_var() {
234        let r = TypeScriptRenderer;
235        let opts = RenderOptions::inline();
236
237        let value = Value::EnvVar {
238            name: "DATABASE_URL".into(),
239            by_ref: false,
240        };
241        assert_eq!(r.render_value(&value, &opts), "process.env.DATABASE_URL");
242    }
243
244    #[test]
245    fn test_render_enum_variant() {
246        let r = TypeScriptRenderer;
247        let opts = RenderOptions::inline();
248
249        let value = Value::EnumVariant {
250            path: "JournalMode".into(),
251            variant: "Wal".into(),
252        };
253        assert_eq!(r.render_value(&value, &opts), "JournalMode.Wal");
254    }
255
256    #[test]
257    fn test_render_builder_inline() {
258        let r = TypeScriptRenderer;
259
260        let spec = BuilderSpec::new("PoolOptions")
261            .call_arg("max_connections", Value::uint(10))
262            .call_arg("min_connections", Value::uint(5));
263
264        assert_eq!(
265            spec.render_inline(&r),
266            "new PoolOptions().maxConnections(10).minConnections(5)"
267        );
268    }
269
270    #[test]
271    fn test_transform_method_name() {
272        let r = TypeScriptRenderer;
273        assert_eq!(r.transform_method_name("max_connections"), "maxConnections");
274        assert_eq!(
275            r.transform_method_name("create_if_missing"),
276            "createIfMissing"
277        );
278    }
279
280    #[test]
281    fn test_constructor_variants() {
282        let r = TypeScriptRenderer;
283
284        assert_eq!(
285            r.render_constructor(&Constructor::static_new("Options")),
286            "new Options()"
287        );
288        assert_eq!(
289            r.render_constructor(&Constructor::class_new("PoolOptions")),
290            "new PoolOptions()"
291        );
292        assert_eq!(
293            r.render_constructor(&Constructor::factory("createOptions")),
294            "createOptions()"
295        );
296    }
297}