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)]
198#[non_exhaustive]
199pub struct OutputData {
200 pub headers: Option<Vec<String>>,
202 pub root: Vec<OutputNode>,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
221 pub rich_json: Option<serde_json::Value>,
222}
223
224impl OutputData {
225 pub fn new() -> Self {
227 Self::default()
228 }
229
230 pub fn text(content: impl Into<String>) -> Self {
234 Self {
235 headers: None,
236 root: vec![OutputNode::text(content)],
237 rich_json: None,
238 }
239 }
240
241 pub fn nodes(nodes: Vec<OutputNode>) -> Self {
243 Self {
244 headers: None,
245 root: nodes,
246 rich_json: None,
247 }
248 }
249
250 pub fn table(headers: Vec<String>, nodes: Vec<OutputNode>) -> Self {
252 Self {
253 headers: Some(headers),
254 root: nodes,
255 rich_json: None,
256 }
257 }
258
259 pub fn with_headers(mut self, headers: Vec<String>) -> Self {
261 self.headers = Some(headers);
262 self
263 }
264
265 pub fn with_rich_json(mut self, value: serde_json::Value) -> Self {
267 self.rich_json = Some(value);
268 self
269 }
270
271 pub fn is_simple_text(&self) -> bool {
273 self.root.len() == 1 && self.root[0].is_text_only()
274 }
275
276 pub fn is_flat(&self) -> bool {
278 self.root.iter().all(|n| !n.has_children())
279 }
280
281 pub fn is_tabular(&self) -> bool {
283 self.root.iter().any(|n| !n.cells.is_empty())
284 }
285
286 pub fn as_text(&self) -> Option<&str> {
288 if self.is_simple_text() {
289 self.root[0].text.as_deref()
290 } else {
291 None
292 }
293 }
294
295 pub fn into_text(mut self) -> Result<String, Self> {
299 if self.root.len() == 1 && self.root[0].is_text_only() {
300 Ok(self.root.pop().and_then(|n| n.text).unwrap_or_default())
301 } else {
302 Err(self)
303 }
304 }
305
306 pub fn estimated_byte_size(&self) -> usize {
311 if self.root.len() == 1 && self.root[0].is_text_only() {
312 return self.root[0].text.as_ref().map_or(0, |t| t.len());
313 }
314
315 if self.is_flat() {
316 let mut size = 0;
317 for (i, n) in self.root.iter().enumerate() {
318 if i > 0 {
319 size += 1; }
321 size += n.display_name().len();
322 for cell in &n.cells {
323 size += 1 + cell.len(); }
325 }
326 return size;
327 }
328
329 let mut size = 0;
331 for (i, n) in self.root.iter().enumerate() {
332 if i > 0 {
333 size += 1; }
335 size += n.estimated_byte_size();
336 }
337 size
338 }
339
340 pub fn write_canonical(&self, w: &mut dyn std::io::Write, budget: Option<usize>) -> std::io::Result<usize> {
346 let mut written = 0usize;
347 let budget = budget.unwrap_or(usize::MAX);
348
349 if self.root.len() == 1 && self.root[0].is_text_only() {
350 if let Some(ref text) = self.root[0].text {
351 w.write_all(text.as_bytes())?;
352 return Ok(text.len());
353 }
354 return Ok(0);
355 }
356
357 if self.is_flat() {
358 for (i, n) in self.root.iter().enumerate() {
359 if i > 0 {
360 w.write_all(b"\n")?;
361 written += 1;
362 }
363 let name = n.display_name();
364 w.write_all(name.as_bytes())?;
365 written += name.len();
366 for cell in &n.cells {
367 w.write_all(b"\t")?;
368 w.write_all(cell.as_bytes())?;
369 written += 1 + cell.len();
370 }
371 if written > budget {
372 return Ok(written);
373 }
374 }
375 return Ok(written);
376 }
377
378 for (i, n) in self.root.iter().enumerate() {
380 if i > 0 {
381 w.write_all(b"\n")?;
382 written += 1;
383 }
384 written += n.write_canonical(w, budget.saturating_sub(written))?;
385 if written > budget {
386 return Ok(written);
387 }
388 }
389 Ok(written)
390 }
391
392 pub fn to_canonical_string(&self) -> String {
401 if let Some(text) = self.as_text() {
402 return text.to_string();
403 }
404
405 if self.is_flat() {
407 return self.root.iter()
408 .map(|n| {
409 if n.cells.is_empty() {
410 n.display_name().to_string()
411 } else {
412 let mut parts = vec![n.display_name().to_string()];
414 parts.extend(n.cells.iter().cloned());
415 parts.join("\t")
416 }
417 })
418 .collect::<Vec<_>>()
419 .join("\n");
420 }
421
422 fn format_node(node: &OutputNode) -> String {
424 if node.children.is_empty() {
425 node.name.clone()
426 } else {
427 let children: Vec<String> = node.children.iter()
428 .map(format_node)
429 .collect();
430 format!("{}/{{{}}}", node.name, children.join(","))
431 }
432 }
433
434 self.root.iter()
435 .map(format_node)
436 .collect::<Vec<_>>()
437 .join("\n")
438 }
439
440 pub fn to_json(&self) -> serde_json::Value {
451 if let Some(rich) = &self.rich_json {
454 return rich.clone();
455 }
456 if let Some(text) = self.as_text() {
458 return serde_json::Value::String(text.to_string());
459 }
460
461 if let Some(ref headers) = self.headers {
466 fn row_to_json(node: &OutputNode, headers: &[String]) -> serde_json::Value {
467 let mut map = serde_json::Map::new();
468 if let Some(first) = headers.first() {
470 map.insert(first.clone(), serde_json::Value::String(node.name.clone()));
471 }
472 for (header, cell) in headers.iter().skip(1).zip(node.cells.iter()) {
474 map.insert(header.clone(), serde_json::Value::String(cell.clone()));
475 }
476 if !node.children.is_empty() {
477 let children: Vec<serde_json::Value> = node
478 .children
479 .iter()
480 .map(|child| row_to_json(child, headers))
481 .collect();
482 map.insert("children".to_string(), serde_json::Value::Array(children));
483 }
484 serde_json::Value::Object(map)
485 }
486 let rows: Vec<serde_json::Value> = self
487 .root
488 .iter()
489 .map(|node| row_to_json(node, headers))
490 .collect();
491 return serde_json::Value::Array(rows);
492 }
493
494 if !self.is_flat() {
496 fn node_to_json(node: &OutputNode) -> serde_json::Value {
497 if node.children.is_empty() {
498 serde_json::Value::Null
499 } else {
500 let mut map = serde_json::Map::new();
501 for child in &node.children {
502 map.insert(child.name.clone(), node_to_json(child));
503 }
504 serde_json::Value::Object(map)
505 }
506 }
507
508 if self.root.len() == 1 {
510 return node_to_json(&self.root[0]);
511 }
512 let mut map = serde_json::Map::new();
514 for node in &self.root {
515 map.insert(node.name.clone(), node_to_json(node));
516 }
517 return serde_json::Value::Object(map);
518 }
519
520 let items: Vec<serde_json::Value> = self.root.iter()
522 .map(|n| serde_json::Value::String(n.display_name().to_string()))
523 .collect();
524 serde_json::Value::Array(items)
525 }
526}
527
528#[non_exhaustive]
534#[derive(Debug, Clone, Copy, PartialEq, Eq)]
535pub enum OutputFormat {
536 Json,
538}
539
540pub fn apply_output_format(mut result: ExecResult, format: OutputFormat) -> ExecResult {
546 if result.is_bytes() {
549 let envelope = crate::bytes::bytes_to_envelope(result.out_bytes().unwrap_or(&[]));
550 match format {
551 OutputFormat::Json => {
552 result.set_out(
553 serde_json::to_string(&envelope).unwrap_or_else(|_| "null".to_string()),
554 );
555 result.data = Some(crate::result::json_to_value(envelope));
556 result.set_output(None);
557 }
558 }
559 return result;
560 }
561 if !result.has_output() && result.text_out().is_empty() {
562 if !result.ok() && !result.err.is_empty() {
568 match format {
569 OutputFormat::Json => {
570 let mut obj = serde_json::json!({
573 "error": result.err.trim_end_matches('\n'),
574 "code": result.code,
575 });
576 if let Some(data) = &result.data {
581 obj["data"] = crate::result::value_to_json(data);
582 }
583 let out =
584 serde_json::to_string(&obj).unwrap_or_else(|_| "null".to_string());
585 result.set_out(out);
586 result.data = Some(crate::result::json_to_value(obj));
587 }
588 }
589 }
590 return result;
591 }
592 match format {
593 OutputFormat::Json => {
594 if let Some(output) = result.output() {
595 let json_value = output.to_json();
596 result.set_out(serde_json::to_string(&json_value)
598 .unwrap_or_else(|_| "null".to_string()));
599 result.data = Some(crate::result::json_to_value(json_value));
600 } else if let Some(data) = &result.data {
601 let json_out = serde_json::to_string(&crate::result::value_to_json(data))
606 .unwrap_or_else(|_| "null".to_string());
607 result.set_out(json_out);
608 } else {
609 let text = result.text_out().into_owned();
611 let json_out = serde_json::to_string(&text)
612 .unwrap_or_else(|_| "null".to_string());
613 result.data = Some(crate::value::Value::String(text));
614 result.set_out(json_out);
615 }
616 result.set_output(None);
618 result
619 }
620 }
621}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626
627 #[test]
628 fn entry_type_variants() {
629 assert_ne!(EntryType::File, EntryType::Directory);
630 assert_ne!(EntryType::Directory, EntryType::Executable);
631 assert_ne!(EntryType::Executable, EntryType::Symlink);
632 }
633
634 #[test]
635 fn to_json_simple_text() {
636 let output = OutputData::text("hello world");
637 assert_eq!(output.to_json(), serde_json::json!("hello world"));
638 }
639
640 #[test]
641 fn to_json_flat_list() {
642 let output = OutputData::nodes(vec![
643 OutputNode::new("file1"),
644 OutputNode::new("file2"),
645 OutputNode::new("file3"),
646 ]);
647 assert_eq!(output.to_json(), serde_json::json!(["file1", "file2", "file3"]));
648 }
649
650 #[test]
651 fn to_json_table() {
652 let output = OutputData::table(
653 vec!["NAME".into(), "SIZE".into(), "TYPE".into()],
654 vec![
655 OutputNode::new("foo.rs").with_cells(vec!["1024".into(), "file".into()]),
656 OutputNode::new("bar/").with_cells(vec!["4096".into(), "dir".into()]),
657 ],
658 );
659 assert_eq!(output.to_json(), serde_json::json!([
660 {"NAME": "foo.rs", "SIZE": "1024", "TYPE": "file"},
661 {"NAME": "bar/", "SIZE": "4096", "TYPE": "dir"},
662 ]));
663 }
664
665 #[test]
666 fn to_json_table_with_children() {
667 let output = OutputData::table(
672 vec!["NAME".into(), "TYPE".into(), "SIZE".into()],
673 vec![
674 OutputNode::new(".")
675 .with_entry_type(EntryType::Directory)
676 .with_children(vec![
677 OutputNode::new("top.txt").with_cells(vec!["-".into(), "6".into()]),
678 OutputNode::new("a").with_cells(vec!["d".into(), "60".into()]),
679 ]),
680 OutputNode::new("a")
681 .with_entry_type(EntryType::Directory)
682 .with_children(vec![
683 OutputNode::new("mid.txt").with_cells(vec!["-".into(), "3".into()]),
684 ]),
685 ],
686 );
687 assert_eq!(output.to_json(), serde_json::json!([
688 {"NAME": ".", "children": [
689 {"NAME": "top.txt", "TYPE": "-", "SIZE": "6"},
690 {"NAME": "a", "TYPE": "d", "SIZE": "60"},
691 ]},
692 {"NAME": "a", "children": [
693 {"NAME": "mid.txt", "TYPE": "-", "SIZE": "3"},
694 ]},
695 ]));
696 }
697
698 #[test]
699 fn to_json_tree() {
700 let child1 = OutputNode::new("main.rs").with_entry_type(EntryType::File);
701 let child2 = OutputNode::new("utils.rs").with_entry_type(EntryType::File);
702 let subdir = OutputNode::new("lib")
703 .with_entry_type(EntryType::Directory)
704 .with_children(vec![child2]);
705 let root = OutputNode::new("src")
706 .with_entry_type(EntryType::Directory)
707 .with_children(vec![child1, subdir]);
708
709 let output = OutputData::nodes(vec![root]);
710 assert_eq!(output.to_json(), serde_json::json!({
711 "main.rs": null,
712 "lib": {"utils.rs": null},
713 }));
714 }
715
716 #[test]
717 fn to_json_tree_multiple_roots() {
718 let root1 = OutputNode::new("src")
719 .with_entry_type(EntryType::Directory)
720 .with_children(vec![OutputNode::new("main.rs")]);
721 let root2 = OutputNode::new("docs")
722 .with_entry_type(EntryType::Directory)
723 .with_children(vec![OutputNode::new("README.md")]);
724
725 let output = OutputData::nodes(vec![root1, root2]);
726 assert_eq!(output.to_json(), serde_json::json!({
727 "src": {"main.rs": null},
728 "docs": {"README.md": null},
729 }));
730 }
731
732 #[test]
733 fn to_json_empty() {
734 let output = OutputData::new();
735 assert_eq!(output.to_json(), serde_json::json!([]));
736 }
737
738 #[test]
739 fn rich_json_round_trips_through_serde() {
740 let rich = serde_json::json!({"matches": [{"line": 1, "text": "hi"}]});
745 let output = OutputData::new().with_rich_json(rich.clone());
746
747 let encoded = serde_json::to_string(&output).unwrap();
748 let decoded: OutputData = serde_json::from_str(&encoded).unwrap();
749 assert_eq!(
750 decoded.rich_json,
751 Some(rich),
752 "rich_json must survive a serde round trip"
753 );
754 }
755
756 #[test]
757 fn rich_json_none_stays_off_the_wire() {
758 let output = OutputData::nodes(vec![OutputNode::new("f")]);
762 let encoded = serde_json::to_string(&output).unwrap();
763 assert!(
764 !encoded.contains("rich_json"),
765 "a None rich_json must not serialize: {encoded}"
766 );
767 let decoded: OutputData = serde_json::from_str(&encoded).unwrap();
768 assert_eq!(decoded.rich_json, None);
769 }
770
771 #[test]
772 fn apply_output_format_clears_sentinel() {
773 let output = OutputData::table(
774 vec!["NAME".into()],
775 vec![OutputNode::new("test")],
776 );
777 let result = ExecResult::with_output(output);
778 assert!(result.has_output(), "before: sentinel present");
779
780 let formatted = apply_output_format(result, OutputFormat::Json);
781 assert!(!formatted.has_output(), "after Json: sentinel cleared");
782 }
783
784 #[test]
785 fn apply_output_format_no_double_encoding() {
786 let output = OutputData::nodes(vec![
787 OutputNode::new("file1"),
788 OutputNode::new("file2"),
789 ]);
790 let result = ExecResult::with_output(output);
791
792 let after_json = apply_output_format(result, OutputFormat::Json);
793 let json_out = after_json.text_out().into_owned();
794 assert!(!after_json.has_output(), "sentinel cleared by Json");
795
796 let parsed: serde_json::Value = serde_json::from_str(&json_out).expect("valid JSON");
797 assert_eq!(parsed, serde_json::json!(["file1", "file2"]));
798 }
799
800 #[test]
801 fn apply_output_format_populates_data() {
802 let output = OutputData::nodes(vec![
803 OutputNode::new("file1"),
804 OutputNode::new("file2"),
805 ]);
806 let result = ExecResult::with_output(output);
807 assert!(result.data.is_none(), "before: no data on non-text output");
808
809 let formatted = apply_output_format(result, OutputFormat::Json);
810 assert!(formatted.data.is_some(), "after Json: data populated");
811
812 let data = formatted.data.unwrap();
814 assert!(matches!(data, crate::value::Value::Json(_)), "data should be Json variant");
815 if let crate::value::Value::Json(json) = data {
816 assert_eq!(json, serde_json::json!(["file1", "file2"]));
817 }
818 }
819
820 #[test]
821 fn apply_output_format_prefers_structured_data_over_text() {
822 use crate::value::Value;
826 let result = ExecResult::success_with_data("1", Value::Int(1));
827 assert!(!result.has_output(), "no OutputData sentinel on this path");
828
829 let formatted = apply_output_format(result, OutputFormat::Json);
830 let json_out = formatted.text_out().into_owned();
831 let parsed: serde_json::Value = serde_json::from_str(&json_out).expect("valid JSON");
832 assert_eq!(parsed, serde_json::json!(1), "number, not the string \"1\"");
833 assert_eq!(formatted.data, Some(Value::Int(1)));
835 }
836
837 #[test]
838 fn apply_output_format_compact_json() {
839 let output = OutputData::nodes(vec![
840 OutputNode::new("file1"),
841 OutputNode::new("file2"),
842 ]);
843 let result = ExecResult::with_output(output);
844
845 let formatted = apply_output_format(result, OutputFormat::Json);
846 let out = formatted.text_out();
848 assert!(!out.contains('\n'), "should be compact JSON, got: {}", out);
849 assert_eq!(&*out, r#"["file1","file2"]"#);
850 }
851
852 #[test]
853 fn apply_output_format_emits_json_error_object_on_failure() {
854 let result = ExecResult::failure(2, "grep: unknown flag --bogus-flag");
858 assert!(!result.has_output());
859 assert!(result.text_out().is_empty());
860
861 let formatted = apply_output_format(result, OutputFormat::Json);
862 let out = formatted.text_out().into_owned();
863 let parsed: serde_json::Value = serde_json::from_str(&out).expect("valid JSON");
864 assert_eq!(
865 parsed,
866 serde_json::json!({"error": "grep: unknown flag --bogus-flag", "code": 2})
867 );
868 assert!(matches!(formatted.data, Some(crate::value::Value::Json(_))));
870 }
871
872 #[test]
873 fn apply_output_format_preserves_structured_data_on_error() {
874 let mut result = ExecResult::failure(2, "rm: refusing without --recursive");
879 result.data = Some(crate::value::Value::Json(serde_json::json!({
880 "operation": "fs.remove",
881 "paths": ["important.dat"],
882 })));
883
884 let formatted = apply_output_format(result, OutputFormat::Json);
885 let out = formatted.text_out().into_owned();
886 let parsed: serde_json::Value = serde_json::from_str(&out).expect("valid JSON");
887 assert_eq!(parsed["error"], "rm: refusing without --recursive");
888 assert_eq!(parsed["code"], 2);
889 assert_eq!(parsed["data"]["operation"], "fs.remove");
890 match &formatted.data {
892 Some(crate::value::Value::Json(v)) => assert_eq!(v["data"]["operation"], "fs.remove"),
893 other => panic!("expected nested JSON data, got {other:?}"),
894 }
895 }
896
897 #[test]
898 fn apply_output_format_leaves_clean_no_match_empty() {
899 let result = ExecResult::failure(1, "");
902 let formatted = apply_output_format(result, OutputFormat::Json);
903 assert!(formatted.text_out().is_empty());
904 assert!(formatted.data.is_none());
905 }
906
907 #[test]
908 fn apply_output_format_empty_success_stays_empty() {
909 let result = ExecResult::success("");
910 let formatted = apply_output_format(result, OutputFormat::Json);
911 assert!(formatted.text_out().is_empty());
912 assert!(formatted.data.is_none());
913 }
914
915 #[test]
916 fn estimated_byte_size_text_only_node() {
917 let node = OutputNode::text("hello world");
918 assert_eq!(node.estimated_byte_size(), 11);
920 }
921
922 #[test]
923 fn estimated_byte_size_named_node() {
924 let node = OutputNode::new("file.txt");
925 assert_eq!(node.estimated_byte_size(), 8);
926 }
927
928 #[test]
929 fn write_canonical_respects_budget() {
930 let parent = OutputNode::new("root")
931 .with_children(vec![
932 OutputNode::new("aaaa"),
933 OutputNode::new("bbbb"),
934 OutputNode::new("cccc"),
935 ]);
936 let mut buf = Vec::new();
938 let written = parent.write_canonical(&mut buf, 8).unwrap();
939 let output = String::from_utf8(buf).unwrap();
940 assert!(written <= 16, "should respect budget, wrote {} bytes: {}", written, output);
942 assert!(output.starts_with("root"), "should start with root: {}", output);
944 }
945
946 #[test]
947 fn into_text_simple() {
948 let data = OutputData::text("hello");
949 assert_eq!(data.into_text(), Ok("hello".to_string()));
950 }
951
952 #[test]
953 fn into_text_non_simple() {
954 let data = OutputData::nodes(vec![OutputNode::new("a"), OutputNode::new("b")]);
955 assert!(data.into_text().is_err());
956 }
957
958 #[test]
959 fn into_text_empty() {
960 let data = OutputData::text("");
961 assert_eq!(data.into_text(), Ok("".to_string()));
962 }
963}