tree_fs/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use rand::{distributions::Alphanumeric, thread_rng, Rng};
4use serde::Deserialize;
5use std::env;
6use std::fs::File;
7use std::io::Write;
8use std::path::Path;
9use std::path::PathBuf;
10
11use thiserror::Error;
12
13#[cfg(feature = "yaml")]
14#[derive(Error, Debug)]
15pub enum Error {
16    #[error(transparent)]
17    YAML(#[from] serde_yaml::Error),
18    #[error(transparent)]
19    IO(#[from] std::io::Error),
20}
21#[cfg(feature = "yaml")]
22pub type Result<T> = std::result::Result<T, Error>;
23
24/// Represents a file tree structure
25#[derive(Debug, Deserialize)]
26pub struct Tree {
27    /// Root folder where the tree will be created.
28    #[serde(default = "temp_dir")]
29    pub root: PathBuf,
30    #[serde(default)]
31    drop: bool,
32}
33
34/// Represents a file tree structure
35///
36/// # Examples
37///
38/// ```rust
39// <snip id="example-builder">
40/// use tree_fs::TreeBuilder;
41/// let tree_fs = TreeBuilder::default()
42///     .add("test/foo.txt", "bar")
43///     .add_empty("test/folder-a/folder-b/bar.txt")
44///     .create()
45///     .expect("create tree fs");
46/// println!("created successfully in {}", tree_fs.root.display());
47// </snip>
48/// ```
49///
50/// ```rust
51// <snip id="example-drop">
52/// use tree_fs::TreeBuilder;
53/// let tree_fs = TreeBuilder::default()
54///      .add("test/foo.txt", "bar")
55///      .add_empty("test/folder-a/folder-b/bar.txt")
56///      .drop(true)
57///      .create()
58///      .expect("create tree fs");
59///
60/// println!("created successfully in {}", tree_fs.root.display());
61///
62/// let path = tree_fs.root.clone();
63/// assert!(path.exists());
64///
65/// drop(tree_fs);
66/// assert!(!path.exists());
67// </snip>
68/// ```
69#[derive(Debug, Deserialize)]
70pub struct TreeBuilder {
71    /// Root folder where the tree will be created.
72    #[serde(default = "temp_dir")]
73    pub root: PathBuf,
74    /// Flag indicating whether existing files should be overridden.
75    #[serde(default)]
76    override_file: bool,
77    /// List of file metadata entries in the tree.
78    files: Vec<FileMetadata>,
79    #[serde(default)]
80    drop: bool,
81}
82
83/// Represents metadata for a file in the tree.
84#[derive(Debug, Deserialize)]
85pub struct FileMetadata {
86    /// Path of the file relative to the root folder.
87    pub path: PathBuf,
88    /// Optional content to be written to the file.
89    pub content: Option<String>,
90}
91
92impl Default for TreeBuilder {
93    /// Creates a default `Tree` instance with an empty file list,
94    fn default() -> Self {
95        Self {
96            files: vec![],
97            override_file: false,
98            root: temp_dir(),
99            drop: false,
100        }
101    }
102}
103
104impl Drop for Tree {
105    fn drop(&mut self) {
106        if self.drop {
107            let _ = std::fs::remove_dir_all(&self.root);
108        }
109    }
110}
111
112impl TreeBuilder {
113    /// Sets the root folder where the tree will be created.
114    #[must_use]
115    pub fn root_folder<P: AsRef<Path>>(mut self, dir: P) -> Self {
116        self.root = dir.as_ref().to_path_buf();
117        self
118    }
119
120    /// Sets the `override_file` flag, indicating whether existing files should be overridden.
121    #[must_use]
122    pub const fn drop(mut self, yes: bool) -> Self {
123        self.drop = yes;
124        self
125    }
126
127    /// Sets the `override_file` flag, indicating whether existing files should be overridden.
128    #[must_use]
129    pub const fn override_file(mut self, yes: bool) -> Self {
130        self.override_file = yes;
131        self
132    }
133
134    /// Adds a file with content to the tree.
135    #[must_use]
136    pub fn add<P: AsRef<Path>>(mut self, path: P, content: &str) -> Self {
137        self.files.push(FileMetadata {
138            path: path.as_ref().to_path_buf(),
139            content: Some(content.to_string()),
140        });
141        self
142    }
143
144    /// Adds a file with a empty content.
145    #[must_use]
146    pub fn add_empty<P: AsRef<Path>>(mut self, path: P) -> Self {
147        self.files.push(FileMetadata {
148            path: path.as_ref().to_path_buf(),
149            content: None,
150        });
151        self
152    }
153
154    /// Creates the file tree by generating files and directories based on the specified metadata.
155    ///
156    /// # Errors
157    ///
158    /// Returns an `std::io::Result` indicating success or failure in creating the file tree.
159    pub fn create(&self) -> std::io::Result<Tree> {
160        if !self.root.exists() {
161            std::fs::create_dir_all(&self.root)?;
162        }
163        for file in &self.files {
164            let dest_file = self.root.join(&file.path);
165            if !self.override_file && dest_file.exists() {
166                continue;
167            }
168
169            if let Some(parent_dir) = Path::new(&dest_file).parent() {
170                std::fs::create_dir_all(parent_dir)?;
171            }
172
173            let mut new_file = File::create(&dest_file)?;
174            if let Some(content) = &file.content {
175                new_file.write_all(content.as_bytes())?;
176            }
177        }
178
179        Ok(Tree {
180            root: self.root.clone(),
181            drop: self.drop,
182        })
183    }
184}
185
186#[cfg(feature = "yaml")]
187/// Creates a file tree based on the content of a YAML file.
188///
189/// # Examples
190///
191/// ```rust
192// <snip id="example-from-yaml-file">
193/// use std::path::PathBuf;
194/// let yaml_path = PathBuf::from("tests/fixtures/tree.yaml");
195/// let tree_fs = tree_fs::from_yaml_file(&yaml_path).expect("create tree fs");
196/// assert!(tree_fs.root.exists())
197// </snip>
198/// ```
199///
200/// # Errors
201///
202/// Returns a `Result` containing the path to the root folder of the generated file tree on success,
203/// or an error if the operation fails.
204pub fn from_yaml_file(path: &PathBuf) -> Result<Tree> {
205    let f = std::fs::File::open(path)?;
206    let tree_builder: TreeBuilder = serde_yaml::from_reader(f)?;
207    Ok(tree_builder.create()?)
208}
209
210#[cfg(feature = "yaml")]
211/// Creates a file tree based on a YAML-formatted string.
212///
213/// # Examples
214///
215/// ```rust
216// <snip id="example-from-yaml-str">
217/// let content = r#"
218/// override_file: false
219/// files:
220///   - path: foo.json
221///     content: |
222///       { "foo;": "bar" }
223///   - path: folder/bar.yaml
224///     content: |
225///       foo: bar
226///     "#;
227///
228/// let tree_fs = tree_fs::from_yaml_str(content).expect("create tree fs");
229/// assert!(tree_fs.root.exists())
230// </snip>
231///
232/// ```
233///
234/// # Errors
235/// Returns a `Result` containing the path to the root folder of the generated file tree on success,
236/// or an error if the operation fails.
237pub fn from_yaml_str(content: &str) -> Result<Tree> {
238    let tree_builder: TreeBuilder = serde_yaml::from_str(content)?;
239    Ok(tree_builder.create()?)
240}
241
242fn temp_dir() -> PathBuf {
243    let random_string: String = thread_rng()
244        .sample_iter(&Alphanumeric)
245        .take(5)
246        .map(char::from)
247        .collect();
248
249    env::temp_dir().join(random_string)
250}