Skip to main content

fs_tree/
macros.rs

1//! Macros for declaring a [`FsTree`](crate::FsTree).
2//!
3//! See [`tree!`] for the main macro and syntax documentation.
4
5/// Macro for creating a [`FsTree`](crate::FsTree).
6///
7/// # Syntax:
8///
9/// ## Base syntax:
10///
11/// - `name` represents a regular file with the given `name`.
12/// - `name: [ ... ]` is a directory.
13/// - `name -> name` is a symlink.
14/// - Commas are not accepted.
15///
16/// Here is a simple example:
17///
18/// ```
19/// use fs_tree::{FsTree, tree, TrieMap};
20///
21/// let trie = tree! {
22///     file1
23///     outer_dir: [
24///         file2
25///         inner_dir: [
26///             file3
27///         ]
28///         link1 -> target1
29///         link2 -> target2
30///     ]
31/// };
32///
33/// let expected = FsTree::Directory(TrieMap::from([
34///     ("file1".into(), FsTree::Regular),
35///     ("outer_dir".into(), FsTree::Directory(TrieMap::from([
36///         ("file2".into(), FsTree::Regular),
37///         ("inner_dir".into(), FsTree::Directory(TrieMap::from([
38///             ("file3".into(), FsTree::Regular),
39///         ]))),
40///         ("link1".into(), FsTree::Symlink("target1".into())),
41///         ("link2".into(), FsTree::Symlink("target2".into())),
42///     ]))),
43/// ]));
44///
45/// assert_eq!(trie, expected);
46/// ```
47///
48/// ## Other symbols
49///
50/// If you need symbols like `-` or `.`, you must use `""` (double quotes):
51///
52/// ```
53/// use fs_tree::tree;
54///
55/// let my_tree = tree! {
56///     ".gitignore"
57///     ".config": [
58///         folder1: [
59///             folder2: [
60///                 "complex-!@#%&-filename"
61///             ]
62///         ]
63///     ]
64/// };
65/// ```
66///
67/// If the path you want is stored in a variable, you can insert an expression
68/// by enclosing it in `{}` (curly braces). The expression must implement
69/// `Into<PathBuf>` (e.g., `String`, `&str`, `PathBuf`).
70///
71/// ```
72/// use fs_tree::tree;
73///
74/// use std::path::PathBuf;
75/// # fn random_name() -> String { String::new() }
76/// # fn main() -> std::io::Result<()> {
77/// # struct Link { from: &'static str, to: &'static str }
78///
79/// let hi = "hello ".to_string();
80///
81/// let regular_example = tree! {
82///     hardcoded_file_name
83///     {hi + " world!"}
84///     {100.to_string()}
85///     {PathBuf::from("look im using PathBuf now")}
86/// };
87///
88/// let link = Link { from: "from", to: "to" };
89///
90/// let symlink_example = tree! {
91///     {link.from} -> {link.to}
92/// };
93///
94/// let dir_name = "also works with directories".to_string();
95/// let directory_example = tree! {
96///     {dir_name.to_uppercase()}: [
97///         file1
98///         file2
99///         file3
100///         file4
101///     ]
102/// };
103/// # Ok(()) }
104/// ```
105///
106/// # Return Value
107///
108/// This macro always returns an [`FsTree::Directory`] containing the declared entries.
109///
110/// # Note
111///
112/// If duplicate keys are declared, later entries overwrite earlier ones (standard
113/// [`BTreeMap`](std::collections::BTreeMap) behavior).
114///
115/// # Alternatives
116///
117/// This macro isn't always the easiest way to create an [`FsTree`]. See also:
118/// - [`FsTree::new_dir`] + [`FsTree::insert`] for programmatic construction
119/// - [`FsTree::from_path_text`] for parsing from path strings
120///
121/// [`FsTree`]: crate::FsTree
122/// [`FsTree::Directory`]: crate::FsTree::Directory
123/// [`FsTree::new_dir`]: crate::FsTree::new_dir
124/// [`FsTree::insert`]: crate::FsTree::insert
125/// [`FsTree::from_path_text`]: crate::FsTree::from_path_text
126#[macro_export]
127macro_rules! tree {
128    ($($all:tt)+) => {{
129        let mut trie = $crate::TrieMap::new();
130        // Jumps between tree_internal and inner invocations
131        $crate::tree_internal!(trie $($all)*);
132        $crate::FsTree::Directory(trie)
133    }};
134}
135
136#[doc(hidden)]
137#[macro_export]
138macro_rules! tree_internal {
139    // Base case for recursive macro (lookup tt-munching)
140    ($parent:ident) => {};
141    ($parent:ident $path:ident : $($rest:tt)*) => {
142        $crate::tree_internal_dir!($parent { ::std::stringify!($path) } $($rest)*)
143    };
144    ($parent:ident $path:literal : $($rest:tt)*) => {
145        $crate::tree_internal_dir!($parent { $path } $($rest)*)
146    };
147    ($parent:ident { $path:expr } : $($rest:tt)*) => {
148        $crate::tree_internal_dir!($parent { $path } $($rest)*)
149    };
150
151    // For symlinks we support the cartesian product: S * S, where S := [ident, literal, expr].
152    //
153    // So we have a second step parsing which is done at the other macro.
154    //
155    // For the "FROM -> TO", here we're parsing the FROM while tree_internal_symlink
156    // will parse the TO.
157    ($parent:ident $path:ident -> $($rest:tt)*) => {
158        $crate::tree_internal_symlink!($parent { ::std::stringify!($path) } $($rest)*)
159    };
160    ($parent:ident $path:literal -> $($rest:tt)*) => {
161        $crate::tree_internal_symlink!($parent { $path } $($rest)*)
162    };
163    ($parent:ident { $path:expr } -> $($rest:tt)*) => {
164        $crate::tree_internal_symlink!($parent { $path } $($rest)*)
165    };
166
167    ($parent:ident $path:ident $($rest:tt)*) => {
168        $crate::tree_internal_regular!($parent { ::std::stringify!($path) } $($rest)*);
169    };
170    ($parent:ident $path:literal $($rest:tt)*) => {
171        $crate::tree_internal_regular!($parent { $path } $($rest)*);
172    };
173    ($parent:ident { $path:expr } $($rest:tt)*) => {
174        $crate::tree_internal_regular!($parent { $path } $($rest)*);
175    };
176}
177
178#[doc(hidden)]
179#[macro_export]
180macro_rules! tree_internal_dir {
181    ($parent:ident { $path:expr } [ $($inner:tt)* ] $($rest:tt)*) => {
182        #[allow(unused_mut)]
183        let mut node = $crate::TrieMap::new();
184        $crate::tree_internal!(node $($inner)*);
185        $parent.insert(
186            ::std::path::PathBuf::from($path),
187            $crate::FsTree::Directory(node)
188        );
189        $crate::tree_internal!($parent $($rest)*)
190    };
191}
192
193#[doc(hidden)]
194#[macro_export]
195macro_rules! tree_internal_regular {
196    ($parent:ident { $path:expr } $($rest:tt)*) => {
197        $parent.insert(
198            ::std::path::PathBuf::from($path),
199            $crate::FsTree::Regular
200        );
201        $crate::tree_internal!($parent $($rest)*);
202    };
203}
204
205#[doc(hidden)]
206#[macro_export]
207macro_rules! tree_internal_symlink {
208    // Parse step 2
209    ($parent:ident { $path:expr } $target:ident $($rest:tt)*) => {
210        $crate::tree_internal_symlink!(@done $parent { $path } { ::std::stringify!($target) } $($rest)*)
211    };
212    ($parent:ident { $path:expr } $target:literal $($rest:tt)*) => {
213        $crate::tree_internal_symlink!(@done $parent { $path } { $target } $($rest)*)
214    };
215    ($parent:ident { $path:expr } { $target:expr } $($rest:tt)*) => {
216        $crate::tree_internal_symlink!(@done $parent { $path } { $target } $($rest)*)
217    };
218
219    // All done
220    (@done $parent:ident { $path:expr } { $target:expr } $($rest:tt)*) => {
221        $parent.insert(
222            ::std::path::PathBuf::from($path),
223            $crate::FsTree::Symlink(::std::path::PathBuf::from($target))
224        );
225        $crate::tree_internal!($parent $($rest)*)
226    };
227}
228
229#[cfg(test)]
230mod tests {
231    use std::path::PathBuf;
232
233    use pretty_assertions::assert_eq;
234
235    use crate::{FsTree, TrieMap};
236
237    #[test]
238    fn test_macro_compiles_with_literals_and_idents() {
239        tree! {
240            "folder": [
241                folder: [
242                    file
243                    "file"
244                    link -> target
245                    link -> "target"
246                    "link" -> target
247                    "link" -> "target"
248                ]
249            ]
250        };
251    }
252
253    #[test]
254    fn test_tree_macro_single_regular_file() {
255        let result = tree! {
256            file
257        };
258        let expected = FsTree::Directory(TrieMap::from([("file".into(), FsTree::Regular)]));
259        assert_eq!(result, expected);
260    }
261
262    #[test]
263    fn test_tree_macro_empty_directory() {
264        let result = tree! { dir: [] };
265        let expected = FsTree::Directory(TrieMap::from([("dir".into(), FsTree::new_dir())]));
266        assert_eq!(result, expected);
267    }
268
269    #[test]
270    fn test_tree_macro_single_symlink() {
271        let result = tree! {
272            link -> target
273        };
274
275        let expected = FsTree::Directory(TrieMap::from([(
276            "link".into(),
277            FsTree::Symlink("target".into()),
278        )]));
279
280        assert_eq!(result, expected);
281    }
282
283    #[test]
284    fn test_tree_macro_nested_directories() {
285        let result = tree! {
286            outer_dir: [
287                inner_dir: []
288            ]
289        };
290
291        let expected = {
292            let mut tree = FsTree::new_dir();
293            tree.insert("outer_dir", FsTree::Directory(TrieMap::new()));
294            tree.insert("outer_dir/inner_dir", FsTree::Directory(TrieMap::new()));
295            tree
296        };
297
298        assert_eq!(result, expected);
299    }
300
301    #[test]
302    fn test_tree_macro_mixed_types() {
303        let result = tree! {
304            config
305            outer_dir: [
306                file1
307                file2
308            ]
309            link -> target
310        };
311
312        let expected = {
313            let mut tree = FsTree::new_dir();
314            tree.insert("config", FsTree::Regular);
315            tree.insert("outer_dir", FsTree::Directory(TrieMap::new()));
316            tree.insert("outer_dir/file1", FsTree::Regular);
317            tree.insert("outer_dir/file2", FsTree::Regular);
318            tree.insert("link", FsTree::Symlink("target".into()));
319            tree
320        };
321
322        assert_eq!(result, expected);
323    }
324
325    #[rustfmt::skip]
326    #[test]
327    fn test_tree_macro_big_example() {
328        let result = tree! {
329            config1
330            config2
331            outer_dir: [
332                file1
333                file2
334                inner_dir: [
335                    inner1
336                    inner2
337                    inner3
338                    inner_link -> inner_target
339                ]
340            ]
341            link -> target
342            config3
343        };
344
345        let expected = FsTree::Directory(TrieMap::from([
346            ("config1".into(), FsTree::Regular),
347            ("config2".into(), FsTree::Regular),
348            ("outer_dir".into(), FsTree::Directory(TrieMap::from([
349                ("file1".into(), FsTree::Regular),
350                ("file2".into(), FsTree::Regular),
351                ("inner_dir".into(), FsTree::Directory(TrieMap::from([
352                    ("inner1".into(), FsTree::Regular),
353                    ("inner2".into(), FsTree::Regular),
354                    ("inner3".into(), FsTree::Regular),
355                    ("inner_link".into(), FsTree::Symlink("inner_target".into())),
356                ]))),
357            ]))),
358            ("link".into(), FsTree::Symlink("target".into())),
359            ("config3".into(), FsTree::Regular),
360        ]));
361
362        assert_eq!(result, expected);
363    }
364
365    #[rustfmt::skip]
366    #[test]
367    fn test_tree_macro_with_expressions() {
368        let config = |index: i32| format!("config{index}");
369
370        let result = tree! {
371            {config(1)}
372            {"config2".to_string()}
373            "outer_dir": [
374                {{
375                    let mut string = String::new();
376                    string.push_str("file");
377                    string.push('1');
378                    string
379                }}
380                file2
381                {"inner".to_string() + "_" + "dir"}: [
382                    inner1
383                    {{"inner2"}}
384                    inner3
385                    { format!("inner_link") } -> { ["inner_target"][0] }
386                ]
387            ]
388            link -> { PathBuf::from("target") }
389            config3
390        };
391
392        let expected = FsTree::Directory(TrieMap::from([
393            ("config1".into(), FsTree::Regular),
394            ("config2".into(), FsTree::Regular),
395            ("outer_dir".into(), FsTree::Directory(TrieMap::from([
396                ("file1".into(), FsTree::Regular),
397                ("file2".into(), FsTree::Regular),
398                ("inner_dir".into(), FsTree::Directory(TrieMap::from([
399                    ("inner1".into(), FsTree::Regular),
400                    ("inner2".into(), FsTree::Regular),
401                    ("inner3".into(), FsTree::Regular),
402                    ("inner_link".into(), FsTree::Symlink("inner_target".into())),
403                ]))),
404            ]))),
405            ("link".into(), FsTree::Symlink("target".into())),
406            ("config3".into(), FsTree::Regular),
407        ]));
408
409        assert_eq!(result, expected);
410    }
411
412    #[rustfmt::skip]
413    #[test]
414    fn test_tree_macro_with_symlinks_all_possibilities() {
415
416        // Cartesian product S * S where S := [ident, literal, expr]
417        let result = tree! {
418            a1 -> b1
419            a2 -> "b2"
420            a3 -> {"b3"}
421            "a4" -> b4
422            "a5" -> "b5"
423            "a6" -> {"b6"}
424            {"a7"} -> b7
425            {"a8"} -> "b8"
426            {"a9"} -> {"b9"}
427        };
428
429        let expected = FsTree::Directory(TrieMap::from([
430            ("a1".into(), FsTree::Symlink("b1".into())),
431            ("a2".into(), FsTree::Symlink("b2".into())),
432            ("a3".into(), FsTree::Symlink("b3".into())),
433            ("a4".into(), FsTree::Symlink("b4".into())),
434            ("a5".into(), FsTree::Symlink("b5".into())),
435            ("a6".into(), FsTree::Symlink("b6".into())),
436            ("a7".into(), FsTree::Symlink("b7".into())),
437            ("a8".into(), FsTree::Symlink("b8".into())),
438            ("a9".into(), FsTree::Symlink("b9".into())),
439        ]));
440
441        assert_eq!(result, expected);
442    }
443}