baobao_codegen_typescript/ast/
objects.rs1use baobao_codegen::builder::{CodeBuilder, CodeFragment, Renderable};
4
5#[derive(Debug, Clone)]
7pub struct Property {
8 pub key: String,
9 pub value: PropertyValue,
10}
11
12#[derive(Debug, Clone)]
14pub enum PropertyValue {
15 String(String),
17 Raw(String),
19 Object(JsObject),
21 ArrowFn(ArrowFn),
23}
24
25impl Property {
26 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 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 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 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 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#[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#[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 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 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 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 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 pub fn shorthand(mut self, name: impl Into<String>) -> Self {
140 self.properties.push(Property::shorthand(name));
141 self
142 }
143
144 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 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 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 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 pub fn is_empty(&self) -> bool {
186 self.properties.is_empty()
187 }
188
189 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 pub fn build(&self) -> String {
226 self.render(CodeBuilder::typescript()).build()
227 }
228
229 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 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}