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 display_name(&self) -> &str {
124 if self.name.is_empty() {
125 self.text.as_deref().unwrap_or("")
126 } else {
127 &self.name
128 }
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
150#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
151#[serde(default)]
152pub struct OutputData {
153 pub headers: Option<Vec<String>>,
155 pub root: Vec<OutputNode>,
157}
158
159impl OutputData {
160 pub fn new() -> Self {
162 Self::default()
163 }
164
165 pub fn text(content: impl Into<String>) -> Self {
169 Self {
170 headers: None,
171 root: vec![OutputNode::text(content)],
172 }
173 }
174
175 pub fn nodes(nodes: Vec<OutputNode>) -> Self {
177 Self {
178 headers: None,
179 root: nodes,
180 }
181 }
182
183 pub fn table(headers: Vec<String>, nodes: Vec<OutputNode>) -> Self {
185 Self {
186 headers: Some(headers),
187 root: nodes,
188 }
189 }
190
191 pub fn with_headers(mut self, headers: Vec<String>) -> Self {
193 self.headers = Some(headers);
194 self
195 }
196
197 pub fn is_simple_text(&self) -> bool {
199 self.root.len() == 1 && self.root[0].is_text_only()
200 }
201
202 pub fn is_flat(&self) -> bool {
204 self.root.iter().all(|n| !n.has_children())
205 }
206
207 pub fn is_tabular(&self) -> bool {
209 self.root.iter().any(|n| !n.cells.is_empty())
210 }
211
212 pub fn as_text(&self) -> Option<&str> {
214 if self.is_simple_text() {
215 self.root[0].text.as_deref()
216 } else {
217 None
218 }
219 }
220
221 pub fn to_canonical_string(&self) -> String {
230 if let Some(text) = self.as_text() {
231 return text.to_string();
232 }
233
234 if self.is_flat() {
236 return self.root.iter()
237 .map(|n| {
238 if n.cells.is_empty() {
239 n.display_name().to_string()
240 } else {
241 let mut parts = vec![n.display_name().to_string()];
243 parts.extend(n.cells.iter().cloned());
244 parts.join("\t")
245 }
246 })
247 .collect::<Vec<_>>()
248 .join("\n");
249 }
250
251 fn format_node(node: &OutputNode) -> String {
253 if node.children.is_empty() {
254 node.name.clone()
255 } else {
256 let children: Vec<String> = node.children.iter()
257 .map(format_node)
258 .collect();
259 format!("{}/{{{}}}", node.name, children.join(","))
260 }
261 }
262
263 self.root.iter()
264 .map(format_node)
265 .collect::<Vec<_>>()
266 .join("\n")
267 }
268
269 pub fn to_json(&self) -> serde_json::Value {
280 if let Some(text) = self.as_text() {
282 return serde_json::Value::String(text.to_string());
283 }
284
285 if let Some(ref headers) = self.headers {
287 let rows: Vec<serde_json::Value> = self.root.iter().map(|node| {
288 let mut map = serde_json::Map::new();
289 if let Some(first) = headers.first() {
291 map.insert(first.clone(), serde_json::Value::String(node.name.clone()));
292 }
293 for (header, cell) in headers.iter().skip(1).zip(node.cells.iter()) {
295 map.insert(header.clone(), serde_json::Value::String(cell.clone()));
296 }
297 serde_json::Value::Object(map)
298 }).collect();
299 return serde_json::Value::Array(rows);
300 }
301
302 if !self.is_flat() {
304 fn node_to_json(node: &OutputNode) -> serde_json::Value {
305 if node.children.is_empty() {
306 serde_json::Value::Null
307 } else {
308 let mut map = serde_json::Map::new();
309 for child in &node.children {
310 map.insert(child.name.clone(), node_to_json(child));
311 }
312 serde_json::Value::Object(map)
313 }
314 }
315
316 if self.root.len() == 1 {
318 return node_to_json(&self.root[0]);
319 }
320 let mut map = serde_json::Map::new();
322 for node in &self.root {
323 map.insert(node.name.clone(), node_to_json(node));
324 }
325 return serde_json::Value::Object(map);
326 }
327
328 let items: Vec<serde_json::Value> = self.root.iter()
330 .map(|n| serde_json::Value::String(n.display_name().to_string()))
331 .collect();
332 serde_json::Value::Array(items)
333 }
334}
335
336#[non_exhaustive]
342#[derive(Debug, Clone, Copy, PartialEq, Eq)]
343pub enum OutputFormat {
344 Json,
346}
347
348pub fn apply_output_format(mut result: ExecResult, format: OutputFormat) -> ExecResult {
354 if result.output.is_none() && result.out.is_empty() {
355 return result;
356 }
357 match format {
358 OutputFormat::Json => {
359 let json_str = if let Some(ref output) = result.output {
360 serde_json::to_string_pretty(&output.to_json())
361 .unwrap_or_else(|_| "null".to_string())
362 } else {
363 serde_json::to_string(&result.out)
365 .unwrap_or_else(|_| "null".to_string())
366 };
367 result.out = json_str;
368 result.output = None;
370 result
371 }
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378
379 #[test]
380 fn entry_type_variants() {
381 assert_ne!(EntryType::File, EntryType::Directory);
382 assert_ne!(EntryType::Directory, EntryType::Executable);
383 assert_ne!(EntryType::Executable, EntryType::Symlink);
384 }
385
386 #[test]
387 fn to_json_simple_text() {
388 let output = OutputData::text("hello world");
389 assert_eq!(output.to_json(), serde_json::json!("hello world"));
390 }
391
392 #[test]
393 fn to_json_flat_list() {
394 let output = OutputData::nodes(vec![
395 OutputNode::new("file1"),
396 OutputNode::new("file2"),
397 OutputNode::new("file3"),
398 ]);
399 assert_eq!(output.to_json(), serde_json::json!(["file1", "file2", "file3"]));
400 }
401
402 #[test]
403 fn to_json_table() {
404 let output = OutputData::table(
405 vec!["NAME".into(), "SIZE".into(), "TYPE".into()],
406 vec![
407 OutputNode::new("foo.rs").with_cells(vec!["1024".into(), "file".into()]),
408 OutputNode::new("bar/").with_cells(vec!["4096".into(), "dir".into()]),
409 ],
410 );
411 assert_eq!(output.to_json(), serde_json::json!([
412 {"NAME": "foo.rs", "SIZE": "1024", "TYPE": "file"},
413 {"NAME": "bar/", "SIZE": "4096", "TYPE": "dir"},
414 ]));
415 }
416
417 #[test]
418 fn to_json_tree() {
419 let child1 = OutputNode::new("main.rs").with_entry_type(EntryType::File);
420 let child2 = OutputNode::new("utils.rs").with_entry_type(EntryType::File);
421 let subdir = OutputNode::new("lib")
422 .with_entry_type(EntryType::Directory)
423 .with_children(vec![child2]);
424 let root = OutputNode::new("src")
425 .with_entry_type(EntryType::Directory)
426 .with_children(vec![child1, subdir]);
427
428 let output = OutputData::nodes(vec![root]);
429 assert_eq!(output.to_json(), serde_json::json!({
430 "main.rs": null,
431 "lib": {"utils.rs": null},
432 }));
433 }
434
435 #[test]
436 fn to_json_tree_multiple_roots() {
437 let root1 = OutputNode::new("src")
438 .with_entry_type(EntryType::Directory)
439 .with_children(vec![OutputNode::new("main.rs")]);
440 let root2 = OutputNode::new("docs")
441 .with_entry_type(EntryType::Directory)
442 .with_children(vec![OutputNode::new("README.md")]);
443
444 let output = OutputData::nodes(vec![root1, root2]);
445 assert_eq!(output.to_json(), serde_json::json!({
446 "src": {"main.rs": null},
447 "docs": {"README.md": null},
448 }));
449 }
450
451 #[test]
452 fn to_json_empty() {
453 let output = OutputData::new();
454 assert_eq!(output.to_json(), serde_json::json!([]));
455 }
456
457 #[test]
458 fn apply_output_format_clears_sentinel() {
459 let output = OutputData::table(
460 vec!["NAME".into()],
461 vec![OutputNode::new("test")],
462 );
463 let result = ExecResult::with_output(output);
464 assert!(result.output.is_some(), "before: sentinel present");
465
466 let formatted = apply_output_format(result, OutputFormat::Json);
467 assert!(formatted.output.is_none(), "after Json: sentinel cleared");
468 }
469
470 #[test]
471 fn apply_output_format_no_double_encoding() {
472 let output = OutputData::nodes(vec![
473 OutputNode::new("file1"),
474 OutputNode::new("file2"),
475 ]);
476 let result = ExecResult::with_output(output);
477
478 let after_json = apply_output_format(result, OutputFormat::Json);
479 let json_out = after_json.out.clone();
480 assert!(after_json.output.is_none(), "sentinel cleared by Json");
481
482 let parsed: serde_json::Value = serde_json::from_str(&json_out).expect("valid JSON");
483 assert_eq!(parsed, serde_json::json!(["file1", "file2"]));
484 }
485}