quillmark_core/quill/
tree.rs1use std::collections::HashMap;
3use std::error::Error as StdError;
4use std::path::Path;
5#[derive(Debug, Clone)]
7pub enum FileTreeNode {
8 File {
10 contents: Vec<u8>,
12 },
13 Directory {
15 files: HashMap<String, FileTreeNode>,
17 },
18}
19
20impl FileTreeNode {
21 pub fn get_node<P: AsRef<Path>>(&self, path: P) -> Option<&FileTreeNode> {
23 let path = path.as_ref();
24
25 if path == Path::new("") {
27 return Some(self);
28 }
29
30 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 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; }
56 }
57 }
58
59 Some(current_node)
60 }
61
62 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 pub fn file_exists<P: AsRef<Path>>(&self, path: P) -> bool {
72 matches!(self.get_node(path), Some(FileTreeNode::File { .. }))
73 }
74
75 pub fn dir_exists<P: AsRef<Path>>(&self, path: P) -> bool {
77 matches!(self.get_node(path), Some(FileTreeNode::Directory { .. }))
78 }
79
80 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 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 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 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 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 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 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 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 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 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 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 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 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}