fs_tree/
macros.rs

1//! Macros for declaring a [`FsTree`](crate::FsTree).
2
3/// Macro for declaring a [`FsTree`](crate::FsTree) literal.
4///
5/// # Syntax:
6///
7/// - `name` is a regular file.
8/// - `name: { ... }` is a directory.
9/// - `name -> name` is a symlink.
10/// - Commas are (unfortunately) not supported.
11/// - Use quotes (`"name"`) for spaces, dots, etc.
12///
13/// # Examples:
14///
15/// ```
16/// use fs_tree::{FsTree, tree, TrieMap};
17///
18/// let result = tree! {
19///     file1
20///     outer_dir: {
21///         file2
22///         inner_dir: {
23///             file3
24///         }
25///         link1 -> target
26///         link2 -> "/home/username/.gitconfig"
27///     }
28/// };
29///
30/// let expected = FsTree::Directory(TrieMap::from([
31///     ("file1".into(), FsTree::Regular),
32///     ("outer_dir".into(), FsTree::Directory(TrieMap::from([
33///         ("file2".into(), FsTree::Regular),
34///         ("inner_dir".into(), FsTree::Directory(TrieMap::from([
35///             ("file3".into(), FsTree::Regular),
36///         ]))),
37///         ("link1".into(), FsTree::Symlink("target".into())),
38///         ("link2".into(), FsTree::Symlink("/home/username/.gitconfig".into())),
39///     ]))),
40/// ]));
41///
42/// assert_eq!(result, expected);
43/// ```
44#[macro_export]
45macro_rules! tree {
46    ($($all:tt)+) => {{
47        let mut trie = $crate::TrieMap::new();
48        $crate::trees_internal!(trie $($all)*);
49        $crate::FsTree::Directory(trie)
50    }};
51}
52
53#[doc(hidden)]
54#[macro_export]
55macro_rules! trees_internal {
56    // Base case
57    ($parent_trie:ident) => {};
58    // Directory
59    ($parent_trie:ident $path:ident : { $($inner:tt)* } $($rest:tt)*) => {
60        #[allow(unused_mut)]
61        let mut trie = $crate::TrieMap::new();
62        $crate::trees_internal!(trie $($inner)*);
63        $parent_trie.insert(
64            ::std::path::PathBuf::from(stringify!($path)),
65            $crate::FsTree::Directory(trie)
66        );
67        $crate::trees_internal!($parent_trie $($rest)*)
68    };
69    // Directory variation
70    ($parent_trie:ident $path:literal : { $($inner:tt)* } $($rest:tt)*) => {
71        #[allow(unused_mut)]
72        let mut trie = $crate::TrieMap::new();
73        $crate::trees_internal!(trie $($inner)*);
74        $parent_trie.insert(
75            ::std::path::PathBuf::from($path),
76            $crate::FsTree::Directory(trie)
77        );
78        $crate::trees_internal!($parent_trie $($rest)*)
79    };
80    // Symlink
81    ($parent_trie:ident $path:ident -> $target:ident $($rest:tt)*) => {
82        $parent_trie.insert(
83            ::std::path::PathBuf::from(stringify!($path)),
84            $crate::FsTree::Symlink(::std::path::PathBuf::from(stringify!($target)))
85        );
86        $crate::trees_internal!($parent_trie $($rest)*)
87    };
88    // Symlink variation
89    ($parent_trie:ident $path:literal -> $target:ident $($rest:tt)*) => {
90        $parent_trie.insert(
91            ::std::path::PathBuf::from($path),
92            $crate::FsTree::Symlink(::std::path::PathBuf::from(stringify!($target)))
93        );
94        $crate::trees_internal!($parent_trie $($rest)*)
95    };
96    // Symlink variation
97    ($parent_trie:ident $path:ident -> $target:literal $($rest:tt)*) => {
98        $parent_trie.insert(
99            ::std::path::PathBuf::from(stringify!($path)),
100            $crate::FsTree::Symlink(::std::path::PathBuf::from($target))
101        );
102        $crate::trees_internal!($parent_trie $($rest)*)
103    };
104    // Symlink variation
105    ($parent_trie:ident $path:literal -> $target:literal $($rest:tt)*) => {
106        $parent_trie.insert(
107            ::std::path::PathBuf::from($path),
108            $crate::FsTree::Symlink(::std::path::PathBuf::from($target))
109        );
110        $crate::trees_internal!($parent_trie $($rest)*)
111    };
112    // Regular file
113    ($parent_trie:ident $path:ident $($rest:tt)*) => {
114        $parent_trie.insert(
115            ::std::path::PathBuf::from(stringify!($path)),
116            $crate::FsTree::Regular
117        );
118        $crate::trees_internal!($parent_trie $($rest)*);
119    };
120    // Regular file
121    ($parent_trie:ident $path:literal $($rest:tt)*) => {
122        $parent_trie.insert(
123            ::std::path::PathBuf::from($path),
124            $crate::FsTree::Regular
125        );
126        $crate::trees_internal!($parent_trie $($rest)*);
127    };
128}
129
130#[cfg(test)]
131mod tests {
132    use pretty_assertions::assert_eq;
133
134    use crate::{FsTree, TrieMap};
135
136    #[test]
137    fn test_macro_compiles_with_literals_and_idents() {
138        tree! {
139            "folder": {
140                folder: {
141                    file
142                    "file"
143                    link -> target
144                    link -> "target"
145                    "link" -> target
146                    "link" -> "target"
147                }
148            }
149        };
150    }
151
152    #[test]
153    fn test_tree_macro_single_regular_file() {
154        let result = tree! {
155            file
156        };
157
158        let expected = FsTree::Directory(TrieMap::from([("file".into(), FsTree::Regular)]));
159
160        assert_eq!(result, expected);
161    }
162
163    #[test]
164    fn test_tree_macro_empty_directory() {
165        let result = tree! {
166            dir: {}
167        };
168
169        let expected = FsTree::Directory(TrieMap::from([("dir".into(), FsTree::new_dir())]));
170
171        assert_eq!(result, expected);
172    }
173
174    #[test]
175    fn test_tree_macro_single_symlink() {
176        let result = tree! {
177            link -> target
178        };
179
180        let expected = FsTree::Directory(TrieMap::from([(
181            "link".into(),
182            FsTree::Symlink("target".into()),
183        )]));
184
185        assert_eq!(result, expected);
186    }
187
188    #[test]
189    fn test_tree_macro_nested_directories() {
190        let result = tree! {
191            outer_dir: {
192                inner_dir: {
193                }
194            }
195        };
196
197        let expected = {
198            let mut tree = FsTree::new_dir();
199            tree.insert("outer_dir", FsTree::Directory(TrieMap::new()));
200            tree.insert("outer_dir/inner_dir", FsTree::Directory(TrieMap::new()));
201            tree
202        };
203
204        assert_eq!(result, expected);
205    }
206
207    #[test]
208    fn test_tree_macro_mixed_types() {
209        let result = tree! {
210            config
211            outer_dir: {
212                file1
213                file2
214            }
215            link -> target
216        };
217
218        let expected = {
219            let mut tree = FsTree::new_dir();
220            tree.insert("config", FsTree::Regular);
221            tree.insert("outer_dir", FsTree::Directory(TrieMap::new()));
222            tree.insert("outer_dir/file1", FsTree::Regular);
223            tree.insert("outer_dir/file2", FsTree::Regular);
224            tree.insert("link", FsTree::Symlink("target".into()));
225            tree
226        };
227
228        assert_eq!(result, expected);
229    }
230
231    #[rustfmt::skip]
232    #[test]
233    fn test_tree_macro_big_example() {
234        let result = tree! {
235            config1
236            config2
237            outer_dir: {
238                file1
239                file2
240                inner_dir: {
241                    inner1
242                    inner2
243                    inner3
244                    inner_link -> inner_target
245                }
246            }
247            link -> target
248            config3
249        };
250
251        let expected = FsTree::Directory(TrieMap::from([
252            ("config1".into(), FsTree::Regular),
253            ("config2".into(), FsTree::Regular),
254            ("outer_dir".into(), FsTree::Directory(TrieMap::from([
255                ("file1".into(), FsTree::Regular),
256                ("file2".into(), FsTree::Regular),
257                ("inner_dir".into(), FsTree::Directory(TrieMap::from([
258                    ("inner1".into(), FsTree::Regular),
259                    ("inner2".into(), FsTree::Regular),
260                    ("inner3".into(), FsTree::Regular),
261                    ("inner_link".into(), FsTree::Symlink("inner_target".into())),
262                ]))),
263            ]))),
264            ("link".into(), FsTree::Symlink("target".into())),
265            ("config3".into(), FsTree::Regular),
266        ]));
267
268        assert_eq!(result, expected);
269    }
270}