promkit/core/tree/
node.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
use std::{fs, path};

/// Represents the kind of a node in a tree structure.
///
/// This enum is used to distinguish between nodes that are currently
/// visible in their "folded" state and those that are "unfolded" to reveal
/// their children, if any.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Kind {
    /// Represents a node that is folded (i.e., its children are not currently visible).
    /// - `id`: A unique identifier for the node.
    /// - `path`: The path from the root to this node, represented as a sequence of indices.
    Folded { id: String, path: Path },

    /// Represents a node that is unfolded (i.e., its children are currently visible).
    /// - `id`: A unique identifier for the node.
    /// - `path`: The path from the root to this node, represented as a sequence of indices.
    Unfolded { id: String, path: Path },
}

/// A type alias for a path in the tree, represented as a sequence of indices.
pub type Path = Vec<usize>;

/// Represents a node within a tree structure.
///
/// A node can either be a `NonLeaf`, containing children and a visibility flag,
/// or a `Leaf`, representing an end node without children.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Node {
    /// Represents a non-leaf node, which can contain child nodes.
    /// - `id`: A unique identifier for the node.
    /// - `children`: A vector of child nodes.
    /// - `children_visible`: A boolean indicating if the children of this node are visible.
    NonLeaf {
        id: String,
        children: Vec<Node>,
        children_visible: bool,
    },

    /// Represents a leaf node, which does not contain any child nodes.
    /// - `id`: A unique identifier for the leaf node.
    Leaf(String),
}

impl TryFrom<&path::PathBuf> for Node {
    /// Attempts to create a `Node` from a given directory path.
    ///
    /// This method constructs a `Node::NonLeaf` representing the directory specified by `dir_path`.
    /// It recursively explores the directory, converting subdirectories into `Node::NonLeaf` instances
    /// and files into `Node::Leaf` instances. Directories and files are kept in separate lists initially,
    /// then combined with all directories first, followed by files. Both lists are sorted alphabetically
    /// before merging. The resulting tree structure reflects the hierarchy of files and directories within
    /// `dir_path`, with directories listed before files, both in alphabetical order.
    ///
    /// # Parameters
    /// - `dir_path`: A reference to a `PathBuf` representing the directory to be converted into a `Node`.
    ///
    /// # Returns
    /// A `Result` containing the root `Node` of the constructed tree if successful, or an `Error` if the
    /// directory cannot be read or if any file name cannot be converted to a string.
    ///
    /// # Errors
    /// This method returns an `Error` if:
    /// - The path does not exist or is not a directory.
    /// - There is an error reading the directory contents.
    /// - A file name cannot be converted to a UTF-8 string.
    type Error = anyhow::Error;

    fn try_from(dir_path: &path::PathBuf) -> anyhow::Result<Self> {
        let mut directories = Vec::new();
        let mut files = Vec::new();

        if dir_path.is_dir() {
            for entry in fs::read_dir(dir_path)? {
                let path = entry?.path();
                if path.is_dir() {
                    directories.push(Node::try_from(&path)?);
                } else if path.is_file() {
                    files.push(Node::Leaf(
                        path.file_name()
                            .and_then(|name| name.to_str())
                            .ok_or_else(|| {
                                std::io::Error::new(
                                    std::io::ErrorKind::Other,
                                    "Failed to convert file name to string",
                                )
                            })?
                            .to_string(),
                    ));
                }
            }
        }

        directories.sort_by(|a, b| a.id().cmp(b.id()));
        files.sort_by(|a, b| a.id().cmp(b.id()));

        let mut children = directories;
        children.extend(files);

        Ok(Node::NonLeaf {
            id: dir_path
                .file_name()
                .and_then(|name| name.to_str())
                .ok_or_else(|| {
                    std::io::Error::new(
                        std::io::ErrorKind::Other,
                        "Failed to convert directory name to string",
                    )
                })?
                .to_string(),
            children,
            children_visible: false,
        })
    }
}

impl Node {
    fn id(&self) -> &String {
        match self {
            Node::NonLeaf { id, .. } => id,
            Node::Leaf(id) => id,
        }
    }

    /// Flattens the tree structure into a vector of `Kind`, including only visible nodes.
    ///
    /// This method performs a depth-first search (DFS) to traverse the tree and collect
    /// nodes into a vector. Each node is represented as either `Kind::Folded` or `Kind::Unfolded`
    /// based on its visibility and whether it has children.
    ///
    /// Returns:
    /// - Vec<Kind>: A vector of `Kind` representing the visible nodes in the tree.
    pub fn flatten_visibles(&self) -> Vec<Kind> {
        fn dfs(node: &Node, path: Path, ret: &mut Vec<Kind>) {
            match node {
                Node::NonLeaf {
                    id,
                    children,
                    children_visible,
                } => {
                    if *children_visible {
                        ret.push(Kind::Unfolded {
                            id: id.clone(),
                            path: path.clone(),
                        });
                        for (index, child) in children.iter().enumerate() {
                            let mut new_path = path.clone();
                            new_path.push(index);
                            dfs(child, new_path, ret);
                        }
                    } else {
                        ret.push(Kind::Folded {
                            id: id.clone(),
                            path: path.clone(),
                        });
                    }
                }
                Node::Leaf(item) => {
                    ret.push(Kind::Folded {
                        id: item.clone(),
                        path: path.clone(),
                    });
                }
            }
        }

        let mut ret = Vec::new();
        dfs(self, Vec::new(), &mut ret);
        ret
    }

    /// Toggles the visibility of the children of the node specified by the given path.
    ///
    /// Parameters:
    /// - path: &Path - A reference to a vector of usize, representing the path to the target node.
    ///
    /// This method modifies the tree in-place. If the target node is found and is a `NonLeaf`,
    /// its `children_visible` field is toggled.
    pub fn toggle(&mut self, path: &Path) {
        if let Some(Node::NonLeaf {
            children_visible, ..
        }) = self.get_mut(path)
        {
            *children_visible = !*children_visible;
        }
    }

    /// Retrieves the IDs of all nodes along the path to a specified node.
    ///
    /// Parameters:
    /// - path: &Path - A reference to a vector of usize, representing the path to the target node.
    ///
    /// Returns:
    /// - Vec<String>: A vector of String IDs representing the nodes along the path to the target node.
    pub fn get_waypoints(&self, path: &Path) -> Vec<String> {
        let mut ids = Vec::new();
        let mut node = self;
        for &index in path {
            match node {
                Node::NonLeaf { id, children, .. } => {
                    ids.push(id.clone());
                    if let Some(child) = children.get(index) {
                        node = child;
                    } else {
                        break;
                    }
                }
                Node::Leaf(id) => {
                    ids.push(id.clone());
                    break;
                }
            }
        }
        ids
    }

    /// Retrieves a reference to the node specified by the given path.
    ///
    /// Parameters:
    /// - path: &Path - A reference to a vector of usize, representing the path to the target node.
    ///
    /// Returns:
    /// - Option<&Node>: An option containing a reference to the target node if found, or None otherwise.
    pub fn get(&self, path: &Path) -> Option<&Node> {
        let mut node = self;
        for seg in path {
            match node {
                Node::NonLeaf {
                    id: _,
                    children,
                    children_visible: _,
                } => {
                    if let Some(next_node) = children.get(*seg) {
                        node = next_node;
                    } else {
                        return None;
                    }
                }
                Node::Leaf(_) => {
                    return None;
                }
            }
        }
        Some(node)
    }

    /// Retrieves a mutable reference to the node specified by the given path.
    ///
    /// Parameters:
    /// - path: &Path - A reference to a vector of usize, representing the path to the target node.
    ///
    /// Returns:
    /// - Option<&mut Node>: An option containing a mutable reference to the target node if found, or None otherwise.
    pub fn get_mut(&mut self, path: &Path) -> Option<&mut Node> {
        let mut node = self;
        for seg in path {
            match node {
                Node::NonLeaf {
                    id: _,
                    children,
                    children_visible: _,
                } => {
                    if let Some(next_node) = children.get_mut(*seg) {
                        node = next_node;
                    } else {
                        return None;
                    }
                }
                Node::Leaf(_) => {
                    return None;
                }
            }
        }
        Some(node)
    }
}

#[cfg(test)]
mod test {
    use super::*;

    fn create_test_node() -> Node {
        Node::NonLeaf {
            id: "root".into(),
            children: vec![
                Node::NonLeaf {
                    id: "a".into(),
                    children: vec![Node::Leaf("aa".into()), Node::Leaf("ab".into())],
                    children_visible: true,
                },
                Node::Leaf("b".into()),
                Node::Leaf("c".into()),
            ],
            children_visible: true,
        }
    }

    fn as_nonleaf(node: &Node) -> Option<(&String, &Vec<Node>, bool)> {
        match node {
            Node::NonLeaf {
                id,
                children,
                children_visible,
            } => Some((id, children, *children_visible)),
            _ => None,
        }
    }

    mod toggle {
        use super::*;

        #[test]
        fn test() {
            let mut node = create_test_node();
            node.toggle(&vec![]);
            assert!(!as_nonleaf(node.get(&vec![]).unwrap()).unwrap().2);
        }
    }

    mod flatten_visibles {
        use super::*;

        #[test]
        fn test() {
            let node = create_test_node();
            assert_eq!(
                vec![
                    Kind::Unfolded {
                        id: "root".into(),
                        path: vec![],
                    },
                    Kind::Unfolded {
                        id: "a".into(),
                        path: vec![0],
                    },
                    Kind::Folded {
                        id: "aa".into(),
                        path: vec![0, 0],
                    },
                    Kind::Folded {
                        id: "ab".into(),
                        path: vec![0, 1],
                    },
                    Kind::Folded {
                        id: "b".into(),
                        path: vec![1],
                    },
                    Kind::Folded {
                        id: "c".into(),
                        path: vec![2],
                    },
                ],
                node.flatten_visibles(),
            );
        }

        #[test]
        fn test_after_toggle() {
            let mut node = create_test_node();
            node.toggle(&vec![]);
            assert_eq!(
                vec![Kind::Folded {
                    id: "root".into(),
                    path: vec![],
                },],
                node.flatten_visibles(),
            );
        }
    }
}