git_scanner/
flare.rs

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); // TODO: should we return what insert returns? Or self?
45    }
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); // TODO - return self?
52    }
53
54    /// gets a tree entry by path, or None if something along the path doesn't exist
55    #[allow(dead_code)] // used in tests
56    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    /// gets a mutable tree entry by path, or None if something along the path doesn't exist
71    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}