baobao_codegen_typescript/ast/
objects.rs

1//! TypeScript/JavaScript object literal builder.
2
3use baobao_codegen::builder::{CodeBuilder, CodeFragment, Renderable};
4
5use super::arrays::JsArray;
6
7/// A property in a JavaScript object literal.
8#[derive(Debug, Clone)]
9pub struct Property {
10    pub key: String,
11    pub value: PropertyValue,
12}
13
14/// The value of an object property.
15#[derive(Debug, Clone)]
16pub enum PropertyValue {
17    /// A literal string value (will be quoted).
18    String(String),
19    /// A raw expression (will not be quoted).
20    Raw(String),
21    /// A nested object.
22    Object(JsObject),
23    /// An arrow function body.
24    ArrowFn(ArrowFn),
25    /// An array literal.
26    Array(JsArray),
27}
28
29impl Property {
30    /// Create a property with a string value (will be quoted).
31    pub fn string(key: impl Into<String>, value: impl Into<String>) -> Self {
32        Self {
33            key: key.into(),
34            value: PropertyValue::String(value.into()),
35        }
36    }
37
38    /// Create a property with a raw expression value (will not be quoted).
39    pub fn raw(key: impl Into<String>, value: impl Into<String>) -> Self {
40        Self {
41            key: key.into(),
42            value: PropertyValue::Raw(value.into()),
43        }
44    }
45
46    /// Create a property with a nested object value.
47    pub fn object(key: impl Into<String>, value: JsObject) -> Self {
48        Self {
49            key: key.into(),
50            value: PropertyValue::Object(value),
51        }
52    }
53
54    /// Create a property with an arrow function value.
55    pub fn arrow_fn(key: impl Into<String>, value: ArrowFn) -> Self {
56        Self {
57            key: key.into(),
58            value: PropertyValue::ArrowFn(value),
59        }
60    }
61
62    /// Create a property with an array value.
63    pub fn array(key: impl Into<String>, value: JsArray) -> Self {
64        Self {
65            key: key.into(),
66            value: PropertyValue::Array(value),
67        }
68    }
69
70    /// Create a shorthand property where key equals the variable name.
71    pub fn shorthand(name: impl Into<String>) -> Self {
72        let n = name.into();
73        Self {
74            key: n.clone(),
75            value: PropertyValue::Raw(n),
76        }
77    }
78}
79
80/// An arrow function for use as a property value.
81#[derive(Debug, Clone)]
82pub struct ArrowFn {
83    pub params: String,
84    pub is_async: bool,
85    pub body: Vec<String>,
86}
87
88impl ArrowFn {
89    pub fn new(params: impl Into<String>) -> Self {
90        Self {
91            params: params.into(),
92            is_async: false,
93            body: Vec::new(),
94        }
95    }
96
97    pub fn async_(mut self) -> Self {
98        self.is_async = true;
99        self
100    }
101
102    pub fn body_line(mut self, line: impl Into<String>) -> Self {
103        self.body.push(line.into());
104        self
105    }
106
107    pub fn body_lines(mut self, lines: impl IntoIterator<Item = impl Into<String>>) -> Self {
108        for line in lines {
109            self.body.push(line.into());
110        }
111        self
112    }
113}
114
115/// Builder for JavaScript/TypeScript object literals.
116#[derive(Debug, Clone, Default)]
117pub struct JsObject {
118    properties: Vec<Property>,
119}
120
121impl JsObject {
122    pub fn new() -> Self {
123        Self::default()
124    }
125
126    /// Add a property with a string value (will be quoted).
127    pub fn string(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
128        self.properties.push(Property::string(key, value));
129        self
130    }
131
132    /// Add a property with a raw expression value (will not be quoted).
133    pub fn raw(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
134        self.properties.push(Property::raw(key, value));
135        self
136    }
137
138    /// Add a property with a nested object value.
139    pub fn object(mut self, key: impl Into<String>, value: JsObject) -> Self {
140        self.properties.push(Property::object(key, value));
141        self
142    }
143
144    /// Add an arrow function property.
145    pub fn arrow_fn(mut self, key: impl Into<String>, value: ArrowFn) -> Self {
146        self.properties.push(Property::arrow_fn(key, value));
147        self
148    }
149
150    /// Add an array property.
151    pub fn array(mut self, key: impl Into<String>, value: JsArray) -> Self {
152        self.properties.push(Property::array(key, value));
153        self
154    }
155
156    /// Conditionally add an array property.
157    pub fn array_if(self, condition: bool, key: impl Into<String>, value: JsArray) -> Self {
158        if condition {
159            self.array(key, value)
160        } else {
161            self
162        }
163    }
164
165    /// Conditionally add an array property using an Option.
166    pub fn array_opt(self, key: impl Into<String>, value: Option<JsArray>) -> Self {
167        match value {
168            Some(v) => self.array(key, v),
169            None => self,
170        }
171    }
172
173    /// Add a property with a TOML value (converted to TypeScript literal).
174    ///
175    /// - Strings are quoted
176    /// - Numbers and booleans are raw
177    /// - Other types are ignored
178    pub fn toml(self, key: impl Into<String>, value: &toml::Value) -> Self {
179        match value {
180            toml::Value::String(s) => self.string(key, s),
181            toml::Value::Integer(i) => self.raw(key, i.to_string()),
182            toml::Value::Float(f) => self.raw(key, f.to_string()),
183            toml::Value::Boolean(b) => self.raw(key, b.to_string()),
184            _ => self,
185        }
186    }
187
188    /// Conditionally add a TOML value property.
189    pub fn toml_opt(self, key: impl Into<String>, value: Option<&toml::Value>) -> Self {
190        match value {
191            Some(v) => self.toml(key, v),
192            None => self,
193        }
194    }
195
196    /// Add an IR DefaultValue property, quoting strings appropriately.
197    pub fn default_value(self, key: impl Into<String>, value: &baobao_ir::DefaultValue) -> Self {
198        match value {
199            baobao_ir::DefaultValue::String(s) => self.string(key, s),
200            baobao_ir::DefaultValue::Int(i) => self.raw(key, i.to_string()),
201            baobao_ir::DefaultValue::Float(f) => self.raw(key, f.to_string()),
202            baobao_ir::DefaultValue::Bool(b) => self.raw(key, b.to_string()),
203        }
204    }
205
206    /// Conditionally add an IR DefaultValue property.
207    pub fn default_value_opt(
208        self,
209        key: impl Into<String>,
210        value: Option<&baobao_ir::DefaultValue>,
211    ) -> Self {
212        match value {
213            Some(v) => self.default_value(key, v),
214            None => self,
215        }
216    }
217
218    /// Add a shorthand property where key equals the variable name.
219    pub fn shorthand(mut self, name: impl Into<String>) -> Self {
220        self.properties.push(Property::shorthand(name));
221        self
222    }
223
224    /// Conditionally add a string property.
225    pub fn string_if(
226        self,
227        condition: bool,
228        key: impl Into<String>,
229        value: impl Into<String>,
230    ) -> Self {
231        if condition {
232            self.string(key, value)
233        } else {
234            self
235        }
236    }
237
238    /// Conditionally add a string property using an Option.
239    pub fn string_opt(self, key: impl Into<String>, value: Option<impl Into<String>>) -> Self {
240        match value {
241            Some(v) => self.string(key, v),
242            None => self,
243        }
244    }
245
246    /// Conditionally add a raw property.
247    pub fn raw_if(self, condition: bool, key: impl Into<String>, value: impl Into<String>) -> Self {
248        if condition {
249            self.raw(key, value)
250        } else {
251            self
252        }
253    }
254
255    /// Conditionally add a raw property using an Option.
256    pub fn raw_opt(self, key: impl Into<String>, value: Option<impl Into<String>>) -> Self {
257        match value {
258            Some(v) => self.raw(key, v),
259            None => self,
260        }
261    }
262
263    /// Conditionally add a nested object property.
264    pub fn object_if(self, condition: bool, key: impl Into<String>, value: JsObject) -> Self {
265        if condition {
266            self.object(key, value)
267        } else {
268            self
269        }
270    }
271
272    /// Check if the object is empty.
273    pub fn is_empty(&self) -> bool {
274        self.properties.is_empty()
275    }
276
277    /// Render the object literal to a CodeBuilder.
278    pub fn render(&self, builder: CodeBuilder) -> CodeBuilder {
279        if self.properties.is_empty() {
280            return builder.raw("{}");
281        }
282
283        let builder = builder.line("{").indent();
284        let builder = self.render_properties(builder);
285        builder.dedent().raw("}")
286    }
287
288    fn render_properties(&self, builder: CodeBuilder) -> CodeBuilder {
289        self.properties
290            .iter()
291            .fold(builder, |b, prop| match &prop.value {
292                PropertyValue::String(s) => b.line(&format!("{}: \"{}\",", prop.key, s)),
293                PropertyValue::Raw(s) => b.line(&format!("{}: {},", prop.key, s)),
294                PropertyValue::Object(obj) => {
295                    let b = b.line(&format!("{}: {{", prop.key)).indent();
296                    let b = obj.render_properties(b);
297                    b.dedent().line("},")
298                }
299                PropertyValue::ArrowFn(func) => {
300                    let async_kw = if func.is_async { "async " } else { "" };
301                    let b = b.line(&format!(
302                        "{}: {}({}) => {{",
303                        prop.key, async_kw, func.params
304                    ));
305                    let b = b.indent();
306                    let b = func.body.iter().fold(b, |b, line| b.line(line));
307                    b.dedent().line("},")
308                }
309                PropertyValue::Array(arr) => b.line(&format!("{}: {},", prop.key, arr.build())),
310            })
311    }
312
313    /// Build the object as a string.
314    pub fn build(&self) -> String {
315        self.render(CodeBuilder::typescript()).build()
316    }
317
318    /// Convert properties to code fragments.
319    fn properties_to_fragments(&self) -> Vec<CodeFragment> {
320        self.properties
321            .iter()
322            .map(|prop| match &prop.value {
323                PropertyValue::String(s) => CodeFragment::Line(format!("{}: \"{}\",", prop.key, s)),
324                PropertyValue::Raw(s) => CodeFragment::Line(format!("{}: {},", prop.key, s)),
325                PropertyValue::Object(obj) => {
326                    let body = obj.properties_to_fragments();
327                    // Remove trailing comma from nested object for cleaner output
328                    CodeFragment::Block {
329                        header: format!("{}: {{", prop.key),
330                        body,
331                        close: Some("},".to_string()),
332                    }
333                }
334                PropertyValue::ArrowFn(func) => {
335                    let async_kw = if func.is_async { "async " } else { "" };
336                    let body: Vec<CodeFragment> = func
337                        .body
338                        .iter()
339                        .map(|line| CodeFragment::Line(line.clone()))
340                        .collect();
341                    CodeFragment::Block {
342                        header: format!("{}: {}({}) => {{", prop.key, async_kw, func.params),
343                        body,
344                        close: Some("},".to_string()),
345                    }
346                }
347                PropertyValue::Array(arr) => {
348                    CodeFragment::Line(format!("{}: {},", prop.key, arr.build()))
349                }
350            })
351            .collect()
352    }
353}
354
355impl Renderable for JsObject {
356    fn to_fragments(&self) -> Vec<CodeFragment> {
357        if self.properties.is_empty() {
358            return vec![CodeFragment::Raw("{}".to_string())];
359        }
360
361        vec![CodeFragment::Block {
362            header: "{".to_string(),
363            body: self.properties_to_fragments(),
364            close: Some("}".to_string()),
365        }]
366    }
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372
373    #[test]
374    fn test_empty_object() {
375        let obj = JsObject::new().build();
376        assert_eq!(obj, "{}");
377    }
378
379    #[test]
380    fn test_object_with_string() {
381        let obj = JsObject::new().string("name", "myapp").build();
382        assert!(obj.contains("name: \"myapp\","));
383    }
384
385    #[test]
386    fn test_object_with_raw() {
387        let obj = JsObject::new().raw("count", "42").build();
388        assert!(obj.contains("count: 42,"));
389    }
390
391    #[test]
392    fn test_object_with_shorthand() {
393        let obj = JsObject::new().shorthand("helloCommand").build();
394        assert!(obj.contains("helloCommand: helloCommand,"));
395    }
396
397    #[test]
398    fn test_nested_object() {
399        let inner = JsObject::new().raw("foo", "fooCommand");
400        let outer = JsObject::new()
401            .string("name", "test")
402            .object("commands", inner);
403        let result = outer.build();
404        assert!(result.contains("name: \"test\","));
405        assert!(result.contains("commands:"));
406        assert!(result.contains("foo: fooCommand,"));
407    }
408
409    #[test]
410    fn test_conditional_string() {
411        let obj = JsObject::new()
412            .string("name", "test")
413            .string_opt("desc", Some("A description"))
414            .string_opt("missing", None::<&str>)
415            .build();
416        assert!(obj.contains("name: \"test\","));
417        assert!(obj.contains("desc: \"A description\","));
418        assert!(!obj.contains("missing"));
419    }
420
421    #[test]
422    fn test_arrow_fn() {
423        let func = ArrowFn::new("{ args, options }")
424            .async_()
425            .body_line("console.log(args);")
426            .body_line("await run(args);");
427        let obj = JsObject::new().arrow_fn("action", func).build();
428        assert!(obj.contains("action: async ({ args, options }) => {"));
429        assert!(obj.contains("console.log(args);"));
430        assert!(obj.contains("await run(args);"));
431        assert!(obj.contains("},"));
432    }
433}