baobao_codegen_typescript/ast/
objects.rs

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