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        #[allow(unused_mut)]
130        let mut trie = $crate::TrieMap::new();
131        // Jumps between tree_internal and inner invocations
132        $crate::tree_internal!(trie $($all)*);
133        $crate::FsTree::Directory(trie)
134    }};
135}
136
137#[doc(hidden)]
138#[macro_export]
139macro_rules! tree_internal {
140    // Base case for recursive macro (lookup tt-munching)
141    ($parent:ident) => {};
142    ($parent:ident $path:ident : $($rest:tt)*) => {
143        $crate::tree_internal_dir!($parent { ::std::stringify!($path) } $($rest)*)
144    };
145    ($parent:ident $path:literal : $($rest:tt)*) => {
146        $crate::tree_internal_dir!($parent { $path } $($rest)*)
147    };
148    ($parent:ident { $path:expr } : $($rest:tt)*) => {
149        $crate::tree_internal_dir!($parent { $path } $($rest)*)
150    };
151
152    // For symlinks we support the cartesian product: S * S, where S := [ident, literal, expr].
153    //
154    // So we have a second step parsing which is done at the other macro.
155    //
156    // For the "FROM -> TO", here we're parsing the FROM while tree_internal_symlink
157    // will parse the TO.
158    ($parent:ident $path:ident -> $($rest:tt)*) => {
159        $crate::tree_internal_symlink!($parent { ::std::stringify!($path) } $($rest)*)
160    };
161    ($parent:ident $path:literal -> $($rest:tt)*) => {
162        $crate::tree_internal_symlink!($parent { $path } $($rest)*)
163    };
164    ($parent:ident { $path:expr } -> $($rest:tt)*) => {
165        $crate::tree_internal_symlink!($parent { $path } $($rest)*)
166    };
167
168    ($parent:ident $path:ident $($rest:tt)*) => {
169        $crate::tree_internal_regular!($parent { ::std::stringify!($path) } $($rest)*);
170    };
171    ($parent:ident $path:literal $($rest:tt)*) => {
172        $crate::tree_internal_regular!($parent { $path } $($rest)*);
173    };
174    ($parent:ident { $path:expr } $($rest:tt)*) => {
175        $crate::tree_internal_regular!($parent { $path } $($rest)*);
176    };
177}
178
179#[doc(hidden)]
180#[macro_export]
181macro_rules! tree_internal_dir {
182    ($parent:ident { $path:expr } [ $($inner:tt)* ] $($rest:tt)*) => {
183        #[allow(unused_mut)]
184        let mut node = $crate::TrieMap::new();
185        $crate::tree_internal!(node $($inner)*);
186        $parent.insert(
187            ::std::path::PathBuf::from($path),
188            $crate::FsTree::Directory(node)
189        );
190        $crate::tree_internal!($parent $($rest)*)
191    };
192}
193
194#[doc(hidden)]
195#[macro_export]
196macro_rules! tree_internal_regular {
197    ($parent:ident { $path:expr } $($rest:tt)*) => {
198        $parent.insert(
199            ::std::path::PathBuf::from($path),
200            $crate::FsTree::Regular
201        );
202        $crate::tree_internal!($parent $($rest)*);
203    };
204}
205
206#[doc(hidden)]
207#[macro_export]
208macro_rules! tree_internal_symlink {
209    // Parse step 2
210    ($parent:ident { $path:expr } $target:ident $($rest:tt)*) => {
211        $crate::tree_internal_symlink!(@done $parent { $path } { ::std::stringify!($target) } $($rest)*)
212    };
213    ($parent:ident { $path:expr } $target:literal $($rest:tt)*) => {
214        $crate::tree_internal_symlink!(@done $parent { $path } { $target } $($rest)*)
215    };
216    ($parent:ident { $path:expr } { $target:expr } $($rest:tt)*) => {
217        $crate::tree_internal_symlink!(@done $parent { $path } { $target } $($rest)*)
218    };
219
220    // All done
221    (@done $parent:ident { $path:expr } { $target:expr } $($rest:tt)*) => {
222        $parent.insert(
223            ::std::path::PathBuf::from($path),
224            $crate::FsTree::Symlink(::std::path::PathBuf::from($target))
225        );
226        $crate::tree_internal!($parent $($rest)*)
227    };
228}
229
230#[cfg(test)]
231mod tests {
232    use std::path::PathBuf;
233
234    use pretty_assertions::assert_eq;
235
236    use crate::{FsTree, TrieMap};
237
238    #[test]
239    fn test_tree_macro_empty() {
240        assert_eq!(tree! {}, FsTree::Directory(TrieMap::new()));
241    }
242
243    #[test]
244    fn test_tree_macro_compiles_with_literals_and_idents() {
245        tree! {
246            "folder": [
247                folder: [
248                    file
249                    "file"
250                    link -> target
251                    link -> "target"
252                    "link" -> target
253                    "link" -> "target"
254                ]
255            ]
256        };
257    }
258
259    #[test]
260    fn test_tree_macro_single_regular_file() {
261        let result = tree! {
262            file
263        };
264        let expected = FsTree::Directory(TrieMap::from([("file".into(), FsTree::Regular)]));
265        assert_eq!(result, expected);
266    }
267
268    #[test]
269    fn test_tree_macro_empty_directory() {
270        let result = tree! { dir: [] };
271        let expected = FsTree::Directory(TrieMap::from([("dir".into(), FsTree::new_dir())]));
272        assert_eq!(result, expected);
273    }
274
275    #[test]
276    fn test_tree_macro_single_symlink() {
277        let result = tree! {
278            link -> target
279        };
280
281        let expected = FsTree::Directory(TrieMap::from([(
282            "link".into(),
283            FsTree::Symlink("target".into()),
284        )]));
285
286        assert_eq!(result, expected);
287    }
288
289    #[test]
290    fn test_tree_macro_nested_directories() {
291        let result = tree! {
292            outer_dir: [
293                inner_dir: []
294            ]
295        };
296
297        let expected = {
298            let mut tree = FsTree::new_dir();
299            tree.insert("outer_dir", FsTree::Directory(TrieMap::new()));
300            tree.insert("outer_dir/inner_dir", FsTree::Directory(TrieMap::new()));
301            tree
302        };
303
304        assert_eq!(result, expected);
305    }
306
307    #[test]
308    fn test_tree_macro_mixed_types() {
309        let result = tree! {
310            config
311            outer_dir: [
312                file1
313                file2
314            ]
315            link -> target
316        };
317
318        let expected = {
319            let mut tree = FsTree::new_dir();
320            tree.insert("config", FsTree::Regular);
321            tree.insert("outer_dir", FsTree::Directory(TrieMap::new()));
322            tree.insert("outer_dir/file1", FsTree::Regular);
323            tree.insert("outer_dir/file2", FsTree::Regular);
324            tree.insert("link", FsTree::Symlink("target".into()));
325            tree
326        };
327
328        assert_eq!(result, expected);
329    }
330
331    #[rustfmt::skip]
332    #[test]
333    fn test_tree_macro_big_example() {
334        let result = tree! {
335            config1
336            config2
337            outer_dir: [
338                file1
339                file2
340                inner_dir: [
341                    inner1
342                    inner2
343                    inner3
344                    inner_link -> inner_target
345                ]
346            ]
347            link -> target
348            config3
349        };
350
351        let expected = FsTree::Directory(TrieMap::from([
352            ("config1".into(), FsTree::Regular),
353            ("config2".into(), FsTree::Regular),
354            ("outer_dir".into(), FsTree::Directory(TrieMap::from([
355                ("file1".into(), FsTree::Regular),
356                ("file2".into(), FsTree::Regular),
357                ("inner_dir".into(), FsTree::Directory(TrieMap::from([
358                    ("inner1".into(), FsTree::Regular),
359                    ("inner2".into(), FsTree::Regular),
360                    ("inner3".into(), FsTree::Regular),
361                    ("inner_link".into(), FsTree::Symlink("inner_target".into())),
362                ]))),
363            ]))),
364            ("link".into(), FsTree::Symlink("target".into())),
365            ("config3".into(), FsTree::Regular),
366        ]));
367
368        assert_eq!(result, expected);
369    }
370
371    #[rustfmt::skip]
372    #[test]
373    fn test_tree_macro_with_expressions() {
374        let config = |index: i32| format!("config{index}");
375
376        let result = tree! {
377            {config(1)}
378            {"config2".to_string()}
379            "outer_dir": [
380                {{
381                    let mut string = String::new();
382                    string.push_str("file");
383                    string.push('1');
384                    string
385                }}
386                file2
387                {"inner".to_string() + "_" + "dir"}: [
388                    inner1
389                    {{"inner2"}}
390                    inner3
391                    { format!("inner_link") } -> { ["inner_target"][0] }
392                ]
393            ]
394            link -> { PathBuf::from("target") }
395            config3
396        };
397
398        let expected = FsTree::Directory(TrieMap::from([
399            ("config1".into(), FsTree::Regular),
400            ("config2".into(), FsTree::Regular),
401            ("outer_dir".into(), FsTree::Directory(TrieMap::from([
402                ("file1".into(), FsTree::Regular),
403                ("file2".into(), FsTree::Regular),
404                ("inner_dir".into(), FsTree::Directory(TrieMap::from([
405                    ("inner1".into(), FsTree::Regular),
406                    ("inner2".into(), FsTree::Regular),
407                    ("inner3".into(), FsTree::Regular),
408                    ("inner_link".into(), FsTree::Symlink("inner_target".into())),
409                ]))),
410            ]))),
411            ("link".into(), FsTree::Symlink("target".into())),
412            ("config3".into(), FsTree::Regular),
413        ]));
414
415        assert_eq!(result, expected);
416    }
417
418    #[rustfmt::skip]
419    #[test]
420    fn test_tree_macro_with_symlinks_all_possibilities() {
421
422        // Cartesian product S * S where S := [ident, literal, expr]
423        let result = tree! {
424            a1 -> b1
425            a2 -> "b2"
426            a3 -> {"b3"}
427            "a4" -> b4
428            "a5" -> "b5"
429            "a6" -> {"b6"}
430            {"a7"} -> b7
431            {"a8"} -> "b8"
432            {"a9"} -> {"b9"}
433        };
434
435        let expected = FsTree::Directory(TrieMap::from([
436            ("a1".into(), FsTree::Symlink("b1".into())),
437            ("a2".into(), FsTree::Symlink("b2".into())),
438            ("a3".into(), FsTree::Symlink("b3".into())),
439            ("a4".into(), FsTree::Symlink("b4".into())),
440            ("a5".into(), FsTree::Symlink("b5".into())),
441            ("a6".into(), FsTree::Symlink("b6".into())),
442            ("a7".into(), FsTree::Symlink("b7".into())),
443            ("a8".into(), FsTree::Symlink("b8".into())),
444            ("a9".into(), FsTree::Symlink("b9".into())),
445        ]));
446
447        assert_eq!(result, expected);
448    }
449}