1use baobao_codegen::builder::{CodeBuilder, CodeFragment, Renderable};
4
5use super::arrays::JsArray;
6
7#[derive(Debug, Clone)]
9pub struct Property {
10 pub key: String,
11 pub value: PropertyValue,
12}
13
14#[derive(Debug, Clone)]
16pub enum PropertyValue {
17 String(String),
19 Raw(String),
21 Object(JsObject),
23 ArrowFn(ArrowFn),
25 Array(JsArray),
27}
28
29impl Property {
30 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 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 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 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 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 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#[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#[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 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 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 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 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 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 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 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 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 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 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 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 pub fn shorthand(mut self, name: impl Into<String>) -> Self {
220 self.properties.push(Property::shorthand(name));
221 self
222 }
223
224 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 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 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 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 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 pub fn is_empty(&self) -> bool {
274 self.properties.is_empty()
275 }
276
277 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 pub fn build(&self) -> String {
315 self.render(CodeBuilder::typescript()).build()
316 }
317
318 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 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}