baobao_codegen_typescript/
renderer.rs1use baobao_codegen::builder::{
7 Binding, Block, BuilderSpec, Constructor, RenderOptions, Renderer, Terminal, Value,
8};
9use baobao_core::to_camel_case;
10
11#[derive(Debug, Clone, Copy, Default)]
15pub struct TypeScriptRenderer;
16
17impl TypeScriptRenderer {
18 pub fn new() -> Self {
20 Self
21 }
22
23 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 Value::Float(v) => v.to_string(),
40 Value::String(v) => format!("\"{}\"", v),
41 Value::Ident(v) => v.clone(),
42 Value::Duration { millis } => {
43 millis.to_string()
45 }
46 Value::EnumVariant { path, variant } => {
47 format!("{}.{}", path, variant)
49 }
50 Value::EnvVar { name, .. } => {
51 format!("process.env.{}", name)
53 }
54 Value::Try(inner) => {
55 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 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 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 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 let mut result = String::from("(() => {\n");
122
123 for binding in &block.bindings {
125 result.push_str(&self.render_binding(binding, &inner_opts));
126 result.push('\n');
127 }
128
129 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 to_camel_case(name)
144 }
145
146 fn render_constructor(&self, ctor: &Constructor) -> String {
147 match ctor {
148 Constructor::StaticNew { type_path } => {
149 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 }
190
191 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 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}