Skip to main content

st/formatters/
json.rs

1use super::Formatter;
2use crate::scanner::{FileNode, TreeStats};
3use anyhow::Result;
4use serde_json::{json, Value};
5use std::collections::HashMap;
6use std::io::Write;
7use std::path::{Path, PathBuf};
8
9pub struct JsonFormatter {
10    pub compact: bool,
11}
12
13impl JsonFormatter {
14    pub fn new(compact: bool) -> Self {
15        Self { compact }
16    }
17
18    fn build_json_tree(&self, nodes: &[FileNode], root_path: &Path) -> Value {
19        // Build parent-child relationships
20        let mut children_map: HashMap<PathBuf, Vec<&FileNode>> = HashMap::new();
21        let mut root_node = None;
22
23        for node in nodes {
24            if node.path == root_path {
25                root_node = Some(node);
26            } else if let Some(parent) = node.path.parent() {
27                children_map
28                    .entry(parent.to_path_buf())
29                    .or_default()
30                    .push(node);
31            }
32        }
33
34        fn node_to_json(
35            node: &FileNode,
36            children_map: &HashMap<PathBuf, Vec<&FileNode>>,
37            _root_path: &Path,
38        ) -> Value {
39            let name = node
40                .path
41                .file_name()
42                .unwrap_or(node.path.as_os_str())
43                .to_string_lossy()
44                .to_string();
45
46            let mut obj = json!({
47                "name": name,
48                "type": match node.file_type {
49                    crate::scanner::FileType::Directory => "directory",
50                    crate::scanner::FileType::RegularFile => "file",
51                    crate::scanner::FileType::Symlink => "symlink",
52                    crate::scanner::FileType::Executable => "executable",
53                    crate::scanner::FileType::Socket => "socket",
54                    crate::scanner::FileType::Pipe => "pipe",
55                    crate::scanner::FileType::BlockDevice => "block_device",
56                    crate::scanner::FileType::CharDevice => "char_device",
57                },
58            });
59
60            // Only add size for files, not directories
61            if !node.is_dir {
62                obj["size"] = json!(node.size);
63            }
64
65            // Add flags only if they're true
66            if node.permission_denied {
67                obj["permission_denied"] = json!(true);
68            }
69
70            if node.is_ignored {
71                obj["ignored"] = json!(true);
72            }
73
74            if node.is_hidden {
75                obj["hidden"] = json!(true);
76            }
77
78            if node.is_symlink {
79                obj["symlink"] = json!(true);
80            }
81
82            // Add children for directories
83            if let Some(children) = children_map.get(&node.path) {
84                let mut sorted_children = children.to_vec();
85                sorted_children.sort_by(|a, b| match (a.is_dir, b.is_dir) {
86                    (true, false) => std::cmp::Ordering::Less,
87                    (false, true) => std::cmp::Ordering::Greater,
88                    _ => a.path.file_name().cmp(&b.path.file_name()),
89                });
90
91                obj["children"] = json!(sorted_children
92                    .iter()
93                    .map(|child| node_to_json(child, children_map, _root_path))
94                    .collect::<Vec<_>>());
95            }
96
97            obj
98        }
99
100        if let Some(root) = root_node {
101            node_to_json(root, &children_map, root_path)
102        } else {
103            json!({})
104        }
105    }
106}
107
108impl Formatter for JsonFormatter {
109    fn format(
110        &self,
111        writer: &mut dyn Write,
112        nodes: &[FileNode],
113        _stats: &TreeStats,
114        root_path: &Path,
115    ) -> Result<()> {
116        let json_tree = self.build_json_tree(nodes, root_path);
117
118        if self.compact {
119            writeln!(writer, "{}", serde_json::to_string(&json_tree)?)?;
120        } else {
121            writeln!(writer, "{}", serde_json::to_string_pretty(&json_tree)?)?;
122        }
123
124        Ok(())
125    }
126}