Skip to main content

quillmark_core/quill/
tree.rs

1//! In-memory file tree representation for quill bundles.
2use std::collections::HashMap;
3use std::error::Error as StdError;
4use std::path::Path;
5/// A node in the file tree structure
6#[derive(Debug, Clone)]
7pub enum FileTreeNode {
8    /// A file with its contents
9    File {
10        /// The file contents as bytes or UTF-8 string
11        contents: Vec<u8>,
12    },
13    /// A directory containing other files and directories
14    Directory {
15        /// The files and subdirectories in this directory
16        files: HashMap<String, FileTreeNode>,
17    },
18}
19
20impl FileTreeNode {
21    /// Get a file or directory node by path
22    pub fn get_node<P: AsRef<Path>>(&self, path: P) -> Option<&FileTreeNode> {
23        let path = path.as_ref();
24
25        // Handle root path
26        if path == Path::new("") {
27            return Some(self);
28        }
29
30        // Split path into components
31        let components: Vec<_> = path
32            .components()
33            .filter_map(|c| {
34                if let std::path::Component::Normal(s) = c {
35                    s.to_str()
36                } else {
37                    None
38                }
39            })
40            .collect();
41
42        if components.is_empty() {
43            return Some(self);
44        }
45
46        // Navigate through the tree
47        let mut current_node = self;
48        for component in components {
49            match current_node {
50                FileTreeNode::Directory { files } => {
51                    current_node = files.get(component)?;
52                }
53                FileTreeNode::File { .. } => {
54                    return None; // Can't traverse into a file
55                }
56            }
57        }
58
59        Some(current_node)
60    }
61
62    /// Get file contents by path
63    pub fn get_file<P: AsRef<Path>>(&self, path: P) -> Option<&[u8]> {
64        match self.get_node(path)? {
65            FileTreeNode::File { contents } => Some(contents.as_slice()),
66            FileTreeNode::Directory { .. } => None,
67        }
68    }
69
70    /// Check if a file exists at the given path
71    pub fn file_exists<P: AsRef<Path>>(&self, path: P) -> bool {
72        matches!(self.get_node(path), Some(FileTreeNode::File { .. }))
73    }
74
75    /// Check if a directory exists at the given path
76    pub fn dir_exists<P: AsRef<Path>>(&self, path: P) -> bool {
77        matches!(self.get_node(path), Some(FileTreeNode::Directory { .. }))
78    }
79
80    /// List all files in a directory (non-recursive)
81    pub fn list_files<P: AsRef<Path>>(&self, dir_path: P) -> Vec<String> {
82        match self.get_node(dir_path) {
83            Some(FileTreeNode::Directory { files }) => files
84                .iter()
85                .filter_map(|(name, node)| {
86                    if matches!(node, FileTreeNode::File { .. }) {
87                        Some(name.clone())
88                    } else {
89                        None
90                    }
91                })
92                .collect(),
93            _ => Vec::new(),
94        }
95    }
96
97    /// List all subdirectories in a directory (non-recursive)
98    pub fn list_subdirectories<P: AsRef<Path>>(&self, dir_path: P) -> Vec<String> {
99        match self.get_node(dir_path) {
100            Some(FileTreeNode::Directory { files }) => files
101                .iter()
102                .filter_map(|(name, node)| {
103                    if matches!(node, FileTreeNode::Directory { .. }) {
104                        Some(name.clone())
105                    } else {
106                        None
107                    }
108                })
109                .collect(),
110            _ => Vec::new(),
111        }
112    }
113
114    /// Insert a file or directory at the given path
115    pub fn insert<P: AsRef<Path>>(
116        &mut self,
117        path: P,
118        node: FileTreeNode,
119    ) -> Result<(), Box<dyn StdError + Send + Sync>> {
120        let path = path.as_ref();
121
122        // Split path into components
123        let components: Vec<_> = path
124            .components()
125            .filter_map(|c| {
126                if let std::path::Component::Normal(s) = c {
127                    s.to_str().map(|s| s.to_string())
128                } else {
129                    None
130                }
131            })
132            .collect();
133
134        if components.is_empty() {
135            return Err("Cannot insert at root path".into());
136        }
137
138        // Navigate to parent directory, creating directories as needed
139        let mut current_node = self;
140        for component in &components[..components.len() - 1] {
141            match current_node {
142                FileTreeNode::Directory { files } => {
143                    current_node =
144                        files
145                            .entry(component.clone())
146                            .or_insert_with(|| FileTreeNode::Directory {
147                                files: HashMap::new(),
148                            });
149                }
150                FileTreeNode::File { .. } => {
151                    return Err("Cannot traverse into a file".into());
152                }
153            }
154        }
155
156        // Insert the new node
157        let filename = &components[components.len() - 1];
158        match current_node {
159            FileTreeNode::Directory { files } => {
160                files.insert(filename.clone(), node);
161                Ok(())
162            }
163            FileTreeNode::File { .. } => Err("Cannot insert into a file".into()),
164        }
165    }
166
167    /// Parse a tree structure from JSON value
168    pub(crate) fn from_json_value(
169        value: &serde_json::Value,
170    ) -> Result<Self, Box<dyn StdError + Send + Sync>> {
171        if let Some(contents_str) = value.get("contents").and_then(|v| v.as_str()) {
172            // It's a file with string contents
173            Ok(FileTreeNode::File {
174                contents: contents_str.as_bytes().to_vec(),
175            })
176        } else if let Some(bytes_array) = value.get("contents").and_then(|v| v.as_array()) {
177            // It's a file with byte array contents
178            let contents: Vec<u8> = bytes_array
179                .iter()
180                .filter_map(|v| v.as_u64().and_then(|n| u8::try_from(n).ok()))
181                .collect();
182            Ok(FileTreeNode::File { contents })
183        } else if let Some(obj) = value.as_object() {
184            // It's a directory (either empty or with nested files)
185            let mut files = HashMap::new();
186            for (name, child_value) in obj {
187                files.insert(name.clone(), Self::from_json_value(child_value)?);
188            }
189            // Empty directories are valid
190            Ok(FileTreeNode::Directory { files })
191        } else {
192            Err(format!("Invalid file tree node: {:?}", value).into())
193        }
194    }
195
196    pub fn print_tree(&self) -> String {
197        self.print_tree_recursive("", "", true)
198    }
199
200    fn print_tree_recursive(&self, name: &str, prefix: &str, is_last: bool) -> String {
201        let mut result = String::new();
202
203        // Choose the appropriate tree characters
204        let connector = if is_last { "└── " } else { "├── " };
205        let extension = if is_last { "    " } else { "│   " };
206
207        match self {
208            FileTreeNode::File { .. } => {
209                result.push_str(&format!("{}{}{}\n", prefix, connector, name));
210            }
211            FileTreeNode::Directory { files } => {
212                // Add trailing slash for directories like `tree` does
213                result.push_str(&format!("{}{}{}/\n", prefix, connector, name));
214
215                let child_prefix = format!("{}{}", prefix, extension);
216                let count = files.len();
217
218                for (i, (child_name, node)) in files.iter().enumerate() {
219                    let is_last_child = i == count - 1;
220                    result.push_str(&node.print_tree_recursive(
221                        child_name,
222                        &child_prefix,
223                        is_last_child,
224                    ));
225                }
226            }
227        }
228
229        result
230    }
231}