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() + self.text.as_ref().map_or(0, |t| t.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 if written >= budget {
149 return Ok(written);
150 }
151 w.write_all(b"/{")?;
152 written += 2;
153 for (i, child) in self.children.iter().enumerate() {
154 if written >= budget {
155 break;
156 }
157 if i > 0 {
158 w.write_all(b",")?;
159 written += 1;
160 }
161 written += child.write_canonical(w, budget.saturating_sub(written))?;
162 }
163 w.write_all(b"}")?;
164 written += 1;
165 Ok(written)
166 }
167
168 pub fn display_name(&self) -> &str {
170 if self.name.is_empty() {
171 self.text.as_deref().unwrap_or("")
172 } else {
173 &self.name
174 }
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
196#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
197#[serde(default)]
198pub struct OutputData {
199 pub headers: Option<Vec<String>>,
201 pub root: Vec<OutputNode>,
203 #[serde(skip)]
212 #[cfg_attr(feature = "schema", schemars(skip))]
213 pub rich_json: Option<serde_json::Value>,
214}
215
216impl OutputData {
217 pub fn new() -> Self {
219 Self::default()
220 }
221
222 pub fn text(content: impl Into<String>) -> Self {
226 Self {
227 headers: None,
228 root: vec![OutputNode::text(content)],
229 rich_json: None,
230 }
231 }
232
233 pub fn nodes(nodes: Vec<OutputNode>) -> Self {
235 Self {
236 headers: None,
237 root: nodes,
238 rich_json: None,
239 }
240 }
241
242 pub fn table(headers: Vec<String>, nodes: Vec<OutputNode>) -> Self {
244 Self {
245 headers: Some(headers),
246 root: nodes,
247 rich_json: None,
248 }
249 }
250
251 pub fn with_headers(mut self, headers: Vec<String>) -> Self {
253 self.headers = Some(headers);
254 self
255 }
256
257 pub fn with_rich_json(mut self, value: serde_json::Value) -> Self {
259 self.rich_json = Some(value);
260 self
261 }
262
263 pub fn is_simple_text(&self) -> bool {
265 self.root.len() == 1 && self.root[0].is_text_only()
266 }
267
268 pub fn is_flat(&self) -> bool {
270 self.root.iter().all(|n| !n.has_children())
271 }
272
273 pub fn is_tabular(&self) -> bool {
275 self.root.iter().any(|n| !n.cells.is_empty())
276 }
277
278 pub fn as_text(&self) -> Option<&str> {
280 if self.is_simple_text() {
281 self.root[0].text.as_deref()
282 } else {
283 None
284 }
285 }
286
287 pub fn into_text(mut self) -> Result<String, Self> {
291 if self.root.len() == 1 && self.root[0].is_text_only() {
292 Ok(self.root.pop().and_then(|n| n.text).unwrap_or_default())
293 } else {
294 Err(self)
295 }
296 }
297
298 pub fn estimated_byte_size(&self) -> usize {
303 if self.root.len() == 1 && self.root[0].is_text_only() {
304 return self.root[0].text.as_ref().map_or(0, |t| t.len());
305 }
306
307 if self.is_flat() {
308 let mut size = 0;
309 for (i, n) in self.root.iter().enumerate() {
310 if i > 0 {
311 size += 1; }
313 size += n.display_name().len();
314 for cell in &n.cells {
315 size += 1 + cell.len(); }
317 }
318 return size;
319 }
320
321 let mut size = 0;
323 for (i, n) in self.root.iter().enumerate() {
324 if i > 0 {
325 size += 1; }
327 size += n.estimated_byte_size();
328 }
329 size
330 }
331
332 pub fn write_canonical(&self, w: &mut dyn std::io::Write, budget: Option<usize>) -> std::io::Result<usize> {
338 let mut written = 0usize;
339 let budget = budget.unwrap_or(usize::MAX);
340
341 if self.root.len() == 1 && self.root[0].is_text_only() {
342 if let Some(ref text) = self.root[0].text {
343 w.write_all(text.as_bytes())?;
344 return Ok(text.len());
345 }
346 return Ok(0);
347 }
348
349 if self.is_flat() {
350 for (i, n) in self.root.iter().enumerate() {
351 if i > 0 {
352 w.write_all(b"\n")?;
353 written += 1;
354 }
355 let name = n.display_name();
356 w.write_all(name.as_bytes())?;
357 written += name.len();
358 for cell in &n.cells {
359 w.write_all(b"\t")?;
360 w.write_all(cell.as_bytes())?;
361 written += 1 + cell.len();
362 }
363 if written > budget {
364 return Ok(written);
365 }
366 }
367 return Ok(written);
368 }
369
370 for (i, n) in self.root.iter().enumerate() {
372 if i > 0 {
373 w.write_all(b"\n")?;
374 written += 1;
375 }
376 written += n.write_canonical(w, budget.saturating_sub(written))?;
377 if written > budget {
378 return Ok(written);
379 }
380 }
381 Ok(written)
382 }
383
384 pub fn to_canonical_string(&self) -> String {
393 if let Some(text) = self.as_text() {
394 return text.to_string();
395 }
396
397 if self.is_flat() {
399 return self.root.iter()
400 .map(|n| {
401 if n.cells.is_empty() {
402 n.display_name().to_string()
403 } else {
404 let mut parts = vec![n.display_name().to_string()];
406 parts.extend(n.cells.iter().cloned());
407 parts.join("\t")
408 }
409 })
410 .collect::<Vec<_>>()
411 .join("\n");
412 }
413
414 fn format_node(node: &OutputNode) -> String {
416 if node.children.is_empty() {
417 node.name.clone()
418 } else {
419 let children: Vec<String> = node.children.iter()
420 .map(format_node)
421 .collect();
422 format!("{}/{{{}}}", node.name, children.join(","))
423 }
424 }
425
426 self.root.iter()
427 .map(format_node)
428 .collect::<Vec<_>>()
429 .join("\n")
430 }
431
432 pub fn to_json(&self) -> serde_json::Value {
443 if let Some(rich) = &self.rich_json {
446 return rich.clone();
447 }
448 if let Some(text) = self.as_text() {
450 return serde_json::Value::String(text.to_string());
451 }
452
453 if let Some(ref headers) = self.headers {
455 let rows: Vec<serde_json::Value> = self.root.iter().map(|node| {
456 let mut map = serde_json::Map::new();
457 if let Some(first) = headers.first() {
459 map.insert(first.clone(), serde_json::Value::String(node.name.clone()));
460 }
461 for (header, cell) in headers.iter().skip(1).zip(node.cells.iter()) {
463 map.insert(header.clone(), serde_json::Value::String(cell.clone()));
464 }
465 serde_json::Value::Object(map)
466 }).collect();
467 return serde_json::Value::Array(rows);
468 }
469
470 if !self.is_flat() {
472 fn node_to_json(node: &OutputNode) -> serde_json::Value {
473 if node.children.is_empty() {
474 serde_json::Value::Null
475 } else {
476 let mut map = serde_json::Map::new();
477 for child in &node.children {
478 map.insert(child.name.clone(), node_to_json(child));
479 }
480 serde_json::Value::Object(map)
481 }
482 }
483
484 if self.root.len() == 1 {
486 return node_to_json(&self.root[0]);
487 }
488 let mut map = serde_json::Map::new();
490 for node in &self.root {
491 map.insert(node.name.clone(), node_to_json(node));
492 }
493 return serde_json::Value::Object(map);
494 }
495
496 let items: Vec<serde_json::Value> = self.root.iter()
498 .map(|n| serde_json::Value::String(n.display_name().to_string()))
499 .collect();
500 serde_json::Value::Array(items)
501 }
502}
503
504#[non_exhaustive]
510#[derive(Debug, Clone, Copy, PartialEq, Eq)]
511pub enum OutputFormat {
512 Json,
514}
515
516pub fn apply_output_format(mut result: ExecResult, format: OutputFormat) -> ExecResult {
522 if !result.has_output() && result.text_out().is_empty() {
523 return result;
524 }
525 match format {
526 OutputFormat::Json => {
527 if let Some(output) = result.output() {
528 let json_value = output.to_json();
529 result.set_out(serde_json::to_string(&json_value)
531 .unwrap_or_else(|_| "null".to_string()));
532 result.data = Some(crate::result::json_to_value(json_value));
533 } else {
534 let text = result.text_out().into_owned();
536 let json_out = serde_json::to_string(&text)
537 .unwrap_or_else(|_| "null".to_string());
538 result.data = Some(crate::value::Value::String(text));
539 result.set_out(json_out);
540 }
541 result.set_output(None);
543 result
544 }
545 }
546}
547
548#[cfg(test)]
549mod tests {
550 use super::*;
551
552 #[test]
553 fn entry_type_variants() {
554 assert_ne!(EntryType::File, EntryType::Directory);
555 assert_ne!(EntryType::Directory, EntryType::Executable);
556 assert_ne!(EntryType::Executable, EntryType::Symlink);
557 }
558
559 #[test]
560 fn to_json_simple_text() {
561 let output = OutputData::text("hello world");
562 assert_eq!(output.to_json(), serde_json::json!("hello world"));
563 }
564
565 #[test]
566 fn to_json_flat_list() {
567 let output = OutputData::nodes(vec![
568 OutputNode::new("file1"),
569 OutputNode::new("file2"),
570 OutputNode::new("file3"),
571 ]);
572 assert_eq!(output.to_json(), serde_json::json!(["file1", "file2", "file3"]));
573 }
574
575 #[test]
576 fn to_json_table() {
577 let output = OutputData::table(
578 vec!["NAME".into(), "SIZE".into(), "TYPE".into()],
579 vec![
580 OutputNode::new("foo.rs").with_cells(vec!["1024".into(), "file".into()]),
581 OutputNode::new("bar/").with_cells(vec!["4096".into(), "dir".into()]),
582 ],
583 );
584 assert_eq!(output.to_json(), serde_json::json!([
585 {"NAME": "foo.rs", "SIZE": "1024", "TYPE": "file"},
586 {"NAME": "bar/", "SIZE": "4096", "TYPE": "dir"},
587 ]));
588 }
589
590 #[test]
591 fn to_json_tree() {
592 let child1 = OutputNode::new("main.rs").with_entry_type(EntryType::File);
593 let child2 = OutputNode::new("utils.rs").with_entry_type(EntryType::File);
594 let subdir = OutputNode::new("lib")
595 .with_entry_type(EntryType::Directory)
596 .with_children(vec![child2]);
597 let root = OutputNode::new("src")
598 .with_entry_type(EntryType::Directory)
599 .with_children(vec![child1, subdir]);
600
601 let output = OutputData::nodes(vec![root]);
602 assert_eq!(output.to_json(), serde_json::json!({
603 "main.rs": null,
604 "lib": {"utils.rs": null},
605 }));
606 }
607
608 #[test]
609 fn to_json_tree_multiple_roots() {
610 let root1 = OutputNode::new("src")
611 .with_entry_type(EntryType::Directory)
612 .with_children(vec![OutputNode::new("main.rs")]);
613 let root2 = OutputNode::new("docs")
614 .with_entry_type(EntryType::Directory)
615 .with_children(vec![OutputNode::new("README.md")]);
616
617 let output = OutputData::nodes(vec![root1, root2]);
618 assert_eq!(output.to_json(), serde_json::json!({
619 "src": {"main.rs": null},
620 "docs": {"README.md": null},
621 }));
622 }
623
624 #[test]
625 fn to_json_empty() {
626 let output = OutputData::new();
627 assert_eq!(output.to_json(), serde_json::json!([]));
628 }
629
630 #[test]
631 fn apply_output_format_clears_sentinel() {
632 let output = OutputData::table(
633 vec!["NAME".into()],
634 vec![OutputNode::new("test")],
635 );
636 let result = ExecResult::with_output(output);
637 assert!(result.has_output(), "before: sentinel present");
638
639 let formatted = apply_output_format(result, OutputFormat::Json);
640 assert!(!formatted.has_output(), "after Json: sentinel cleared");
641 }
642
643 #[test]
644 fn apply_output_format_no_double_encoding() {
645 let output = OutputData::nodes(vec![
646 OutputNode::new("file1"),
647 OutputNode::new("file2"),
648 ]);
649 let result = ExecResult::with_output(output);
650
651 let after_json = apply_output_format(result, OutputFormat::Json);
652 let json_out = after_json.text_out().into_owned();
653 assert!(!after_json.has_output(), "sentinel cleared by Json");
654
655 let parsed: serde_json::Value = serde_json::from_str(&json_out).expect("valid JSON");
656 assert_eq!(parsed, serde_json::json!(["file1", "file2"]));
657 }
658
659 #[test]
660 fn apply_output_format_populates_data() {
661 let output = OutputData::nodes(vec![
662 OutputNode::new("file1"),
663 OutputNode::new("file2"),
664 ]);
665 let result = ExecResult::with_output(output);
666 assert!(result.data.is_none(), "before: no data on non-text output");
667
668 let formatted = apply_output_format(result, OutputFormat::Json);
669 assert!(formatted.data.is_some(), "after Json: data populated");
670
671 let data = formatted.data.unwrap();
673 assert!(matches!(data, crate::value::Value::Json(_)), "data should be Json variant");
674 if let crate::value::Value::Json(json) = data {
675 assert_eq!(json, serde_json::json!(["file1", "file2"]));
676 }
677 }
678
679 #[test]
680 fn apply_output_format_compact_json() {
681 let output = OutputData::nodes(vec![
682 OutputNode::new("file1"),
683 OutputNode::new("file2"),
684 ]);
685 let result = ExecResult::with_output(output);
686
687 let formatted = apply_output_format(result, OutputFormat::Json);
688 let out = formatted.text_out();
690 assert!(!out.contains('\n'), "should be compact JSON, got: {}", out);
691 assert_eq!(&*out, r#"["file1","file2"]"#);
692 }
693
694 #[test]
695 fn estimated_byte_size_text_only_node() {
696 let node = OutputNode::text("hello world");
697 assert_eq!(node.estimated_byte_size(), 11);
699 }
700
701 #[test]
702 fn estimated_byte_size_named_node() {
703 let node = OutputNode::new("file.txt");
704 assert_eq!(node.estimated_byte_size(), 8);
705 }
706
707 #[test]
708 fn write_canonical_respects_budget() {
709 let parent = OutputNode::new("root")
710 .with_children(vec![
711 OutputNode::new("aaaa"),
712 OutputNode::new("bbbb"),
713 OutputNode::new("cccc"),
714 ]);
715 let mut buf = Vec::new();
717 let written = parent.write_canonical(&mut buf, 8).unwrap();
718 let output = String::from_utf8(buf).unwrap();
719 assert!(written <= 16, "should respect budget, wrote {} bytes: {}", written, output);
721 assert!(output.starts_with("root"), "should start with root: {}", output);
723 }
724
725 #[test]
726 fn into_text_simple() {
727 let data = OutputData::text("hello");
728 assert_eq!(data.into_text(), Ok("hello".to_string()));
729 }
730
731 #[test]
732 fn into_text_non_simple() {
733 let data = OutputData::nodes(vec![OutputNode::new("a"), OutputNode::new("b")]);
734 assert!(data.into_text().is_err());
735 }
736
737 #[test]
738 fn into_text_empty() {
739 let data = OutputData::text("");
740 assert_eq!(data.into_text(), Ok("".to_string()));
741 }
742}