1use serde::{Deserialize, Serialize};
9
10use crate::result::ExecResult;
11
12#[non_exhaustive]
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
22#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
23#[serde(rename_all = "lowercase")]
24pub enum EntryType {
25 #[default]
27 Text,
28 File,
30 Directory,
32 Executable,
34 Symlink,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
49#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
50#[serde(default)]
51pub struct OutputNode {
52 pub name: String,
54 pub entry_type: EntryType,
56 pub text: Option<String>,
65 pub cells: Vec<String>,
67 pub children: Vec<OutputNode>,
69}
70
71impl OutputNode {
72 pub fn new(name: impl Into<String>) -> Self {
74 Self {
75 name: name.into(),
76 ..Default::default()
77 }
78 }
79
80 pub fn text(content: impl Into<String>) -> Self {
82 Self {
83 text: Some(content.into()),
84 ..Default::default()
85 }
86 }
87
88 pub fn with_entry_type(mut self, entry_type: EntryType) -> Self {
90 self.entry_type = entry_type;
91 self
92 }
93
94 pub fn with_cells(mut self, cells: Vec<String>) -> Self {
96 self.cells = cells;
97 self
98 }
99
100 pub fn with_children(mut self, children: Vec<OutputNode>) -> Self {
102 self.children = children;
103 self
104 }
105
106 pub fn with_text(mut self, text: impl Into<String>) -> Self {
108 self.text = Some(text.into());
109 self
110 }
111
112 pub fn is_text_only(&self) -> bool {
114 self.text.is_some() && self.name.is_empty() && self.cells.is_empty() && self.children.is_empty()
115 }
116
117 pub fn has_children(&self) -> bool {
119 !self.children.is_empty()
120 }
121
122 pub fn estimated_byte_size(&self) -> usize {
124 if self.children.is_empty() {
125 self.name.len()
126 } else {
127 let mut size = self.name.len() + 2; for (i, child) in self.children.iter().enumerate() {
130 if i > 0 {
131 size += 1; }
133 size += child.estimated_byte_size();
134 }
135 size + 1 }
137 }
138
139 pub fn write_canonical(&self, w: &mut dyn std::io::Write, _budget: usize) -> std::io::Result<usize> {
141 if self.children.is_empty() {
142 w.write_all(self.name.as_bytes())?;
143 return Ok(self.name.len());
144 }
145 let mut written = 0;
146 w.write_all(self.name.as_bytes())?;
147 written += self.name.len();
148 w.write_all(b"/{")?;
149 written += 2;
150 for (i, child) in self.children.iter().enumerate() {
151 if i > 0 {
152 w.write_all(b",")?;
153 written += 1;
154 }
155 written += child.write_canonical(w, _budget.saturating_sub(written))?;
156 }
157 w.write_all(b"}")?;
158 written += 1;
159 Ok(written)
160 }
161
162 pub fn display_name(&self) -> &str {
164 if self.name.is_empty() {
165 self.text.as_deref().unwrap_or("")
166 } else {
167 &self.name
168 }
169 }
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
190#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
191#[serde(default)]
192pub struct OutputData {
193 pub headers: Option<Vec<String>>,
195 pub root: Vec<OutputNode>,
197}
198
199impl OutputData {
200 pub fn new() -> Self {
202 Self::default()
203 }
204
205 pub fn text(content: impl Into<String>) -> Self {
209 Self {
210 headers: None,
211 root: vec![OutputNode::text(content)],
212 }
213 }
214
215 pub fn nodes(nodes: Vec<OutputNode>) -> Self {
217 Self {
218 headers: None,
219 root: nodes,
220 }
221 }
222
223 pub fn table(headers: Vec<String>, nodes: Vec<OutputNode>) -> Self {
225 Self {
226 headers: Some(headers),
227 root: nodes,
228 }
229 }
230
231 pub fn with_headers(mut self, headers: Vec<String>) -> Self {
233 self.headers = Some(headers);
234 self
235 }
236
237 pub fn is_simple_text(&self) -> bool {
239 self.root.len() == 1 && self.root[0].is_text_only()
240 }
241
242 pub fn is_flat(&self) -> bool {
244 self.root.iter().all(|n| !n.has_children())
245 }
246
247 pub fn is_tabular(&self) -> bool {
249 self.root.iter().any(|n| !n.cells.is_empty())
250 }
251
252 pub fn as_text(&self) -> Option<&str> {
254 if self.is_simple_text() {
255 self.root[0].text.as_deref()
256 } else {
257 None
258 }
259 }
260
261 pub fn estimated_byte_size(&self) -> usize {
266 if self.root.len() == 1 && self.root[0].is_text_only() {
267 return self.root[0].text.as_ref().map_or(0, |t| t.len());
268 }
269
270 if self.is_flat() {
271 let mut size = 0;
272 for (i, n) in self.root.iter().enumerate() {
273 if i > 0 {
274 size += 1; }
276 size += n.display_name().len();
277 for cell in &n.cells {
278 size += 1 + cell.len(); }
280 }
281 return size;
282 }
283
284 let mut size = 0;
286 for (i, n) in self.root.iter().enumerate() {
287 if i > 0 {
288 size += 1; }
290 size += n.estimated_byte_size();
291 }
292 size
293 }
294
295 pub fn write_canonical(&self, w: &mut dyn std::io::Write, budget: Option<usize>) -> std::io::Result<usize> {
301 let mut written = 0usize;
302 let budget = budget.unwrap_or(usize::MAX);
303
304 if self.root.len() == 1 && self.root[0].is_text_only() {
305 if let Some(ref text) = self.root[0].text {
306 w.write_all(text.as_bytes())?;
307 return Ok(text.len());
308 }
309 return Ok(0);
310 }
311
312 if self.is_flat() {
313 for (i, n) in self.root.iter().enumerate() {
314 if i > 0 {
315 w.write_all(b"\n")?;
316 written += 1;
317 }
318 let name = n.display_name();
319 w.write_all(name.as_bytes())?;
320 written += name.len();
321 for cell in &n.cells {
322 w.write_all(b"\t")?;
323 w.write_all(cell.as_bytes())?;
324 written += 1 + cell.len();
325 }
326 if written > budget {
327 return Ok(written);
328 }
329 }
330 return Ok(written);
331 }
332
333 for (i, n) in self.root.iter().enumerate() {
335 if i > 0 {
336 w.write_all(b"\n")?;
337 written += 1;
338 }
339 written += n.write_canonical(w, budget.saturating_sub(written))?;
340 if written > budget {
341 return Ok(written);
342 }
343 }
344 Ok(written)
345 }
346
347 pub fn to_canonical_string(&self) -> String {
356 if let Some(text) = self.as_text() {
357 return text.to_string();
358 }
359
360 if self.is_flat() {
362 return self.root.iter()
363 .map(|n| {
364 if n.cells.is_empty() {
365 n.display_name().to_string()
366 } else {
367 let mut parts = vec![n.display_name().to_string()];
369 parts.extend(n.cells.iter().cloned());
370 parts.join("\t")
371 }
372 })
373 .collect::<Vec<_>>()
374 .join("\n");
375 }
376
377 fn format_node(node: &OutputNode) -> String {
379 if node.children.is_empty() {
380 node.name.clone()
381 } else {
382 let children: Vec<String> = node.children.iter()
383 .map(format_node)
384 .collect();
385 format!("{}/{{{}}}", node.name, children.join(","))
386 }
387 }
388
389 self.root.iter()
390 .map(format_node)
391 .collect::<Vec<_>>()
392 .join("\n")
393 }
394
395 pub fn to_json(&self) -> serde_json::Value {
406 if let Some(text) = self.as_text() {
408 return serde_json::Value::String(text.to_string());
409 }
410
411 if let Some(ref headers) = self.headers {
413 let rows: Vec<serde_json::Value> = self.root.iter().map(|node| {
414 let mut map = serde_json::Map::new();
415 if let Some(first) = headers.first() {
417 map.insert(first.clone(), serde_json::Value::String(node.name.clone()));
418 }
419 for (header, cell) in headers.iter().skip(1).zip(node.cells.iter()) {
421 map.insert(header.clone(), serde_json::Value::String(cell.clone()));
422 }
423 serde_json::Value::Object(map)
424 }).collect();
425 return serde_json::Value::Array(rows);
426 }
427
428 if !self.is_flat() {
430 fn node_to_json(node: &OutputNode) -> serde_json::Value {
431 if node.children.is_empty() {
432 serde_json::Value::Null
433 } else {
434 let mut map = serde_json::Map::new();
435 for child in &node.children {
436 map.insert(child.name.clone(), node_to_json(child));
437 }
438 serde_json::Value::Object(map)
439 }
440 }
441
442 if self.root.len() == 1 {
444 return node_to_json(&self.root[0]);
445 }
446 let mut map = serde_json::Map::new();
448 for node in &self.root {
449 map.insert(node.name.clone(), node_to_json(node));
450 }
451 return serde_json::Value::Object(map);
452 }
453
454 let items: Vec<serde_json::Value> = self.root.iter()
456 .map(|n| serde_json::Value::String(n.display_name().to_string()))
457 .collect();
458 serde_json::Value::Array(items)
459 }
460}
461
462#[non_exhaustive]
468#[derive(Debug, Clone, Copy, PartialEq, Eq)]
469pub enum OutputFormat {
470 Json,
472}
473
474pub fn apply_output_format(mut result: ExecResult, format: OutputFormat) -> ExecResult {
480 if result.output.is_none() && result.text_out().is_empty() {
481 return result;
482 }
483 match format {
484 OutputFormat::Json => {
485 if let Some(ref output) = result.output {
486 let json_value = output.to_json();
487 result.out = serde_json::to_string(&json_value)
488 .unwrap_or_else(|_| "null".to_string());
489 result.data = Some(crate::result::json_to_value(json_value));
490 } else {
491 let text = result.text_out().into_owned();
493 result.out = serde_json::to_string(&text)
494 .unwrap_or_else(|_| "null".to_string());
495 result.data = ExecResult::try_parse_json(&result.out);
496 }
497 result.output = None;
499 result
500 }
501 }
502}
503
504#[cfg(test)]
505mod tests {
506 use super::*;
507
508 #[test]
509 fn entry_type_variants() {
510 assert_ne!(EntryType::File, EntryType::Directory);
511 assert_ne!(EntryType::Directory, EntryType::Executable);
512 assert_ne!(EntryType::Executable, EntryType::Symlink);
513 }
514
515 #[test]
516 fn to_json_simple_text() {
517 let output = OutputData::text("hello world");
518 assert_eq!(output.to_json(), serde_json::json!("hello world"));
519 }
520
521 #[test]
522 fn to_json_flat_list() {
523 let output = OutputData::nodes(vec![
524 OutputNode::new("file1"),
525 OutputNode::new("file2"),
526 OutputNode::new("file3"),
527 ]);
528 assert_eq!(output.to_json(), serde_json::json!(["file1", "file2", "file3"]));
529 }
530
531 #[test]
532 fn to_json_table() {
533 let output = OutputData::table(
534 vec!["NAME".into(), "SIZE".into(), "TYPE".into()],
535 vec![
536 OutputNode::new("foo.rs").with_cells(vec!["1024".into(), "file".into()]),
537 OutputNode::new("bar/").with_cells(vec!["4096".into(), "dir".into()]),
538 ],
539 );
540 assert_eq!(output.to_json(), serde_json::json!([
541 {"NAME": "foo.rs", "SIZE": "1024", "TYPE": "file"},
542 {"NAME": "bar/", "SIZE": "4096", "TYPE": "dir"},
543 ]));
544 }
545
546 #[test]
547 fn to_json_tree() {
548 let child1 = OutputNode::new("main.rs").with_entry_type(EntryType::File);
549 let child2 = OutputNode::new("utils.rs").with_entry_type(EntryType::File);
550 let subdir = OutputNode::new("lib")
551 .with_entry_type(EntryType::Directory)
552 .with_children(vec![child2]);
553 let root = OutputNode::new("src")
554 .with_entry_type(EntryType::Directory)
555 .with_children(vec![child1, subdir]);
556
557 let output = OutputData::nodes(vec![root]);
558 assert_eq!(output.to_json(), serde_json::json!({
559 "main.rs": null,
560 "lib": {"utils.rs": null},
561 }));
562 }
563
564 #[test]
565 fn to_json_tree_multiple_roots() {
566 let root1 = OutputNode::new("src")
567 .with_entry_type(EntryType::Directory)
568 .with_children(vec![OutputNode::new("main.rs")]);
569 let root2 = OutputNode::new("docs")
570 .with_entry_type(EntryType::Directory)
571 .with_children(vec![OutputNode::new("README.md")]);
572
573 let output = OutputData::nodes(vec![root1, root2]);
574 assert_eq!(output.to_json(), serde_json::json!({
575 "src": {"main.rs": null},
576 "docs": {"README.md": null},
577 }));
578 }
579
580 #[test]
581 fn to_json_empty() {
582 let output = OutputData::new();
583 assert_eq!(output.to_json(), serde_json::json!([]));
584 }
585
586 #[test]
587 fn apply_output_format_clears_sentinel() {
588 let output = OutputData::table(
589 vec!["NAME".into()],
590 vec![OutputNode::new("test")],
591 );
592 let result = ExecResult::with_output(output);
593 assert!(result.output.is_some(), "before: sentinel present");
594
595 let formatted = apply_output_format(result, OutputFormat::Json);
596 assert!(formatted.output.is_none(), "after Json: sentinel cleared");
597 }
598
599 #[test]
600 fn apply_output_format_no_double_encoding() {
601 let output = OutputData::nodes(vec![
602 OutputNode::new("file1"),
603 OutputNode::new("file2"),
604 ]);
605 let result = ExecResult::with_output(output);
606
607 let after_json = apply_output_format(result, OutputFormat::Json);
608 let json_out = after_json.out.clone();
609 assert!(after_json.output.is_none(), "sentinel cleared by Json");
610
611 let parsed: serde_json::Value = serde_json::from_str(&json_out).expect("valid JSON");
612 assert_eq!(parsed, serde_json::json!(["file1", "file2"]));
613 }
614
615 #[test]
616 fn apply_output_format_populates_data() {
617 let output = OutputData::nodes(vec![
618 OutputNode::new("file1"),
619 OutputNode::new("file2"),
620 ]);
621 let result = ExecResult::with_output(output);
622 assert!(result.data.is_none(), "before: no data on non-text output");
623
624 let formatted = apply_output_format(result, OutputFormat::Json);
625 assert!(formatted.data.is_some(), "after Json: data populated");
626
627 let data = formatted.data.unwrap();
629 assert!(matches!(data, crate::value::Value::Json(_)), "data should be Json variant");
630 if let crate::value::Value::Json(json) = data {
631 assert_eq!(json, serde_json::json!(["file1", "file2"]));
632 }
633 }
634
635 #[test]
636 fn apply_output_format_compact_json() {
637 let output = OutputData::nodes(vec![
638 OutputNode::new("file1"),
639 OutputNode::new("file2"),
640 ]);
641 let result = ExecResult::with_output(output);
642
643 let formatted = apply_output_format(result, OutputFormat::Json);
644 assert!(!formatted.out.contains('\n'), "should be compact JSON, got: {}", formatted.out);
646 assert_eq!(formatted.out, r#"["file1","file2"]"#);
647 }
648}