1#![warn(clippy::all)]
2
3use serde::ser::SerializeStruct;
4use serde::{Serialize, Serializer};
5use serde_json::Value;
6use std::collections::HashMap;
7use std::ffi::{OsStr, OsString};
8
9pub static ROOT_NAME: &str = "<root>";
10
11#[derive(Debug, PartialEq)]
12pub struct FlareTreeNode {
13 name: OsString,
14 is_file: bool,
15 children: Vec<FlareTreeNode>,
16 data: HashMap<String, Value>,
17}
18
19impl FlareTreeNode {
20 pub fn name(&self) -> &OsString {
21 &self.name
22 }
23
24 pub fn new<S: Into<OsString>>(name: S, is_file: bool) -> FlareTreeNode {
25 FlareTreeNode {
26 name: name.into(),
27 is_file,
28 children: Vec::new(),
29 data: HashMap::new(),
30 }
31 }
32
33 #[cfg(test)]
34 pub fn file<S: Into<OsString>>(name: S) -> Self {
35 Self::new(name, true)
36 }
37
38 #[cfg(test)]
39 pub fn dir<S: Into<OsString>>(name: S) -> Self {
40 Self::new(name, false)
41 }
42
43 pub fn add_data<S: Into<String>>(&mut self, key: S, value: Value) {
44 self.data.insert(key.into(), value); }
46
47 pub fn append_child(&mut self, child: FlareTreeNode) {
48 if self.is_file {
49 panic!("appending child to a directory: {:?}", self)
50 }
51 self.children.push(child); }
53
54 #[allow(dead_code)] pub fn get_in(&self, path: &mut std::path::Components) -> Option<&FlareTreeNode> {
57 match path.next() {
58 Some(first_name) => {
59 let dir_name = first_name.as_os_str();
60 if !self.is_file {
61 let first_match = self.children.iter().find(|c| dir_name == c.name)?;
62 return first_match.get_in(path);
63 }
64 None
65 }
66 None => Some(self),
67 }
68 }
69
70 pub fn get_in_mut(&mut self, path: &mut std::path::Components) -> Option<&mut FlareTreeNode> {
72 match path.next() {
73 Some(first_name) => {
74 let dir_name = first_name.as_os_str();
75 if !self.is_file {
76 let first_match = self.children.iter_mut().find(|c| dir_name == c.name)?;
77 return first_match.get_in_mut(path);
78 }
79 None
80 }
81 None => Some(self),
82 }
83 }
84
85 pub fn get_data(&self, key: &str) -> Option<&Value> {
86 self.data.get(key)
87 }
88
89 pub fn get_children(&self) -> &Vec<FlareTreeNode> {
90 &self.children
91 }
92}
93
94fn name_as_str<S: Serializer>(name: &OsStr) -> Result<&str, S::Error> {
95 name.to_str().ok_or_else(|| {
96 serde::ser::Error::custom(format!("name {:?} contains invalid UTF-8 characters", name))
97 })
98}
99
100impl Serialize for FlareTreeNode {
101 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
102 where
103 S: Serializer,
104 {
105 let mut state = serializer.serialize_struct("FlareTreeNode", 2)?;
106 let name = name_as_str::<S>(&self.name)?;
107 state.serialize_field("name", &name)?;
108 if !self.data.is_empty() {
109 state.serialize_field("data", &self.data)?
110 }
111 if !self.is_file {
112 state.serialize_field("children", &self.children)?;
113 }
114
115 state.end()
116 }
117}
118
119#[cfg(test)]
120mod test {
121 use super::*;
122 use pretty_assertions::assert_eq;
123 use serde_json::json;
124 use std::path::Path;
125 use test_shared::*;
126
127 #[test]
128 fn can_build_tree() {
129 let mut root = FlareTreeNode::dir("root");
130 root.append_child(FlareTreeNode::file("child"));
131
132 assert_eq!(
133 root,
134 FlareTreeNode {
135 name: OsString::from("root"),
136 is_file: false,
137 children: vec![FlareTreeNode {
138 name: OsString::from("child"),
139 is_file: true,
140 data: HashMap::new(),
141 children: Vec::new()
142 }],
143 data: HashMap::new()
144 }
145 )
146 }
147
148 fn build_test_tree() -> FlareTreeNode {
149 let mut root = FlareTreeNode::dir("root");
150 root.append_child(FlareTreeNode::file("root_file_1.txt"));
151 root.append_child(FlareTreeNode::file("root_file_2.txt"));
152 let mut child1 = FlareTreeNode::dir("child1");
153 child1.append_child(FlareTreeNode::file("child1_file_1.txt"));
154 let mut grand_child = FlareTreeNode::dir("grandchild");
155 grand_child.append_child(FlareTreeNode::file("grandchild_file.txt"));
156 child1.append_child(grand_child);
157 child1.append_child(FlareTreeNode::file("child1_file_2.txt"));
158 let mut child2 = FlareTreeNode::dir("child2");
159 child2.add_data("meta", json!("wibble"));
160 let mut child2_file = FlareTreeNode::file("child2_file.txt");
161 let widget_data = json!({
162 "sprockets": 7,
163 "flanges": ["Nigel, Sarah"]
164 });
165 child2_file.add_data("widgets", widget_data);
166 child2.append_child(child2_file);
167 root.append_child(child1);
168 root.append_child(child2);
169 root
170 }
171
172 #[test]
173 fn can_get_elements_from_tree() {
174 let tree = build_test_tree();
175
176 let mut path = std::path::Path::new("child1/grandchild/grandchild_file.txt").components();
177 let grandchild = tree.get_in(&mut path);
178 assert_eq!(
179 grandchild.expect("Grandchild not found!").name(),
180 "grandchild_file.txt"
181 );
182 }
183
184 #[test]
185 fn can_get_top_level_element_from_tree() {
186 let tree = build_test_tree();
187
188 let mut path = std::path::Path::new("child1").components();
189 let child1 = tree.get_in(&mut path);
190 assert_eq!(child1.expect("child1 not found!").name(), "child1");
191
192 let mut path2 = std::path::Path::new("root_file_1.txt").components();
193 let child2 = tree.get_in(&mut path2);
194 assert_eq!(
195 child2.expect("root_file_1 not found!").name(),
196 "root_file_1.txt"
197 );
198 }
199
200 #[test]
201 fn getting_missing_elements_returns_none() {
202 let tree = build_test_tree();
203 let mut path = std::path::Path::new("child1/grandchild/nonesuch").components();
204 let missing = tree.get_in(&mut path);
205 assert_eq!(missing.is_none(), true);
206
207 let mut path2 =
208 Path::new("child1/grandchild/grandchild_file.txt/files_have_no_kids").components();
209 let missing2 = tree.get_in(&mut path2);
210 assert_eq!(missing2.is_none(), true);
211
212 let mut path3 = Path::new("no_file_at_root").components();
213 let missing3 = tree.get_in(&mut path3);
214 assert_eq!(missing3.is_none(), true);
215 }
216
217 #[test]
218 fn can_get_mut_elements_from_tree() {
219 let mut tree = build_test_tree();
220 let grandchild = tree
221 .get_in_mut(&mut Path::new("child1/grandchild/grandchild_file.txt").components())
222 .expect("Grandchild not found!");
223 assert_eq!(grandchild.name(), "grandchild_file.txt");
224 grandchild.name = OsString::from("fish");
225 let grandchild2 = tree.get_in_mut(&mut Path::new("child1/grandchild/fish").components());
226 assert_eq!(grandchild2.expect("fish not found!").name(), "fish");
227
228 let grandchild_dir = tree
229 .get_in_mut(&mut Path::new("child1/grandchild").components())
230 .expect("Grandchild dir not found!");
231 assert_eq!(grandchild_dir.name(), "grandchild");
232 grandchild_dir.append_child(FlareTreeNode::file("new_kid_on_the_block.txt"));
233 let new_kid = tree
234 .get_in_mut(&mut Path::new("child1/grandchild/new_kid_on_the_block.txt").components())
235 .expect("New kid not found!");
236 assert_eq!(new_kid.name(), "new_kid_on_the_block.txt");
237 }
238
239 #[test]
240 fn can_get_json_payloads_from_tree() {
241 let tree = build_test_tree();
242 let file = tree
243 .get_in(&mut Path::new("child2/child2_file.txt").components())
244 .unwrap();
245
246 assert_eq!(file.name(), "child2_file.txt");
247
248 let expected = json!({
249 "sprockets": 7,
250 "flanges": ["Nigel, Sarah"]
251 });
252
253 assert_eq!(&file.data["widgets"], &expected);
254 }
255
256 #[test]
257 fn can_serialize_directory_to_json() {
258 let root = FlareTreeNode::dir("root");
259
260 assert_eq_json_str(
261 &root,
262 r#"{
263 "name":"root",
264 "children": []
265 }"#,
266 )
267 }
268
269 #[test]
270 fn can_serialize_dir_with_data_to_json() {
271 let mut dir = FlareTreeNode::dir("foo");
272 dir.add_data("wibble", json!("fnord"));
273
274 assert_eq_json_str(
275 &dir,
276 r#"{
277 "name":"foo",
278 "data": {"wibble":"fnord"},
279 "children": []
280 }"#,
281 )
282 }
283
284 #[test]
285 fn can_serialize_file_to_json() {
286 let file = FlareTreeNode::file("foo.txt");
287
288 assert_eq_json_str(
289 &file,
290 r#"{
291 "name":"foo.txt"
292 }"#,
293 )
294 }
295
296 #[test]
297 fn can_serialize_file_with_data_to_json() {
298 let mut file = FlareTreeNode::file("foo.txt");
299 file.add_data("wibble", json!("fnord"));
300
301 assert_eq_json_str(
302 &file,
303 r#"{
304 "name":"foo.txt",
305 "data": {"wibble":"fnord"}
306 }"#,
307 )
308 }
309
310 #[test]
311 fn can_serialize_file_with_data_value_to_json() {
312 let mut file = FlareTreeNode::file("foo.txt");
313 let value = json!({"foo": ["bar", "baz", 123]});
314 file.add_data("bat", value);
315
316 assert_eq_json_str(
317 &file,
318 r#"{
319 "name":"foo.txt",
320 "data": {"bat": {"foo": ["bar", "baz", 123]}}
321 }"#,
322 )
323 }
324
325 #[test]
326 fn can_serialize_simple_tree_to_json() {
327 let mut root = FlareTreeNode::dir("root");
328 root.append_child(FlareTreeNode::file("child.txt"));
329 root.append_child(FlareTreeNode::dir("child2"));
330
331 assert_eq_json_value(
332 &root,
333 &json!({
334 "name":"root",
335 "children":[
336 {
337 "name": "child.txt"
338 },
339 {
340 "name":"child2",
341 "children":[]
342 }
343 ]
344 }),
345 )
346 }
347}