render_as_tree/
lib.rs

1#![warn(missing_docs)]
2
3//! A library that allows you to visualize tree data structures in Rust with
4//! output like `tree(1)`, like so:
5//!
6//! ```text
7//! Parent
8//! ├── Child 1
9//! ├── Child 2
10//! │   ├── Grandchild 1
11//! │   └── Grandchild 2
12//! └── Child 3
13//! ```
14//!
15//! This crate was extracted from [ruut](https://github.com/hibachrach/ruut), a CLI intended for doing the same
16//! thing. See that repo if you're interested in executing a tree visualizer from
17//! the commandline or for something that can process common serialized data types
18//! (e.g. JSON).
19
20/// Represents a node in the tree.
21///
22/// Important as [render] takes a [`Node`] as its only parameter.
23///
24/// # Example
25///
26/// ```
27/// use render_as_tree::Node;
28/// struct BasicNode {
29///     pub name: String,
30///     pub children: Vec<BasicNode>,
31/// }
32///
33/// impl BasicNode {
34///     pub fn new(name: String) -> BasicNode {
35///         BasicNode {
36///             name,
37///             children: Vec::new(),
38///         }
39///     }
40/// }
41///
42/// impl Node for BasicNode {
43///     type Iter<'a> = std::slice::Iter<'a, Self>;
44///
45///     fn name(&self) -> &str {
46///         &self.name
47///     }
48///     fn children(&self) -> Self::Iter<'_> {
49///         self.children.iter()
50///     }
51/// }
52/// ```
53pub trait Node {
54    /// An iterator over the children of this node
55    type Iter<'a>: DoubleEndedIterator<Item = &'a Self>
56    where
57        Self: 'a;
58
59    /// What is displayed for this node when rendered
60    fn name(&self) -> &str;
61    /// The immediate children of this node in the tree
62    fn children(&self) -> Self::Iter<'_>;
63}
64
65/// Renders the given [`Node`] in a human-readable format
66///
67/// Here's an example:
68/// ```no_run
69/// vec![
70///     String::from("root - selena"),
71///     String::from("├── child 1 - sam"),
72///     String::from("│   ├── grandchild 1A - burt"),
73///     String::from("│   ├── grandchild 1B - crabbod"),
74///     String::from("│   └── grandchild 1C - mario"),
75///     String::from("└── child 2 - dumptruck"),
76///     String::from("    ├── grandchild 2A - tilly"),
77///     String::from("    └── grandchild 2B - curling iron"),
78/// ];
79/// ```
80pub fn render<T: Node>(node: &T) -> Vec<String> {
81    let mut lines = vec![node.name().to_owned()];
82    let mut children = node.children();
83    let maybe_last_child = children.next_back();
84    let non_last_children: Vec<&T> = children.collect();
85    if let Some(last_child) = maybe_last_child {
86        let child_node_lines = non_last_children.iter().flat_map(|child| {
87            render(*child)
88                .iter()
89                .enumerate()
90                .map(|(idx, child_line)| {
91                    if idx == 0 {
92                        format!("├── {}", child_line)
93                    } else {
94                        format!("│   {}", child_line)
95                    }
96                })
97                .collect::<Vec<String>>()
98        });
99        let last_child_node_lines = render(last_child);
100        let formatted_last_child_node_lines_iter =
101            last_child_node_lines
102                .iter()
103                .enumerate()
104                .map(|(idx, child_line)| {
105                    if idx == 0 {
106                        format!("└── {}", child_line)
107                    } else {
108                        format!("    {}", child_line)
109                    }
110                });
111        let children_lines = child_node_lines.chain(formatted_last_child_node_lines_iter);
112        lines.extend(children_lines);
113    }
114    lines
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[derive(Debug, PartialEq)]
122    struct BasicNode {
123        pub name: String,
124        pub children: Vec<BasicNode>,
125    }
126
127    impl BasicNode {
128        pub fn new(name: String) -> BasicNode {
129            BasicNode {
130                name,
131                children: Vec::new(),
132            }
133        }
134    }
135
136    impl Node for BasicNode {
137        type Iter<'a> = std::slice::Iter<'a, Self>;
138
139        fn name(&self) -> &str {
140            &self.name
141        }
142        fn children(&self) -> Self::Iter<'_> {
143            self.children.iter()
144        }
145    }
146
147    #[test]
148    fn trivial_case() {
149        assert_eq!(
150            render(&BasicNode::new(String::from("beans"))),
151            vec![String::from("beans")]
152        )
153    }
154
155    #[test]
156    fn simple_case() {
157        let root = BasicNode {
158            name: String::from("root - selena"),
159            children: vec![
160                BasicNode {
161                    name: String::from("child 1 - sam"),
162                    children: vec![
163                        BasicNode::new(String::from("grandchild 1A - burt")),
164                        BasicNode::new(String::from("grandchild 1B - crabbod")),
165                        BasicNode::new(String::from("grandchild 1C - mario")),
166                    ],
167                },
168                BasicNode {
169                    name: String::from("child 2 - dumptruck"),
170                    children: vec![
171                        BasicNode::new(String::from("grandchild 2A - tilly")),
172                        BasicNode::new(String::from("grandchild 2B - curling iron")),
173                    ],
174                },
175            ],
176        };
177        assert_eq!(
178            render(&root),
179            vec![
180                String::from("root - selena"),
181                String::from("├── child 1 - sam"),
182                String::from("│   ├── grandchild 1A - burt"),
183                String::from("│   ├── grandchild 1B - crabbod"),
184                String::from("│   └── grandchild 1C - mario"),
185                String::from("└── child 2 - dumptruck"),
186                String::from("    ├── grandchild 2A - tilly"),
187                String::from("    └── grandchild 2B - curling iron"),
188            ]
189        );
190    }
191}