1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
//! Ephemeral creates a temporary project on your filesystem at any location of your choice
//! so that you can use it while testing anything that works on a rust project - mainly cargo
//! commands/binaries. It can be used to generate projects of other languages too.
//!
//! # INSTALLATION:
//!
//! To use this crate, add it to the dev-dependencies since it is used only during testing:
//!
//! ```toml
//! [dev-dependencies]
//! ephemeral = "0.2"
//! ```
//!
//! # USAGE:
//!
//! To create a project:
//!
//! ```rust
//! use ephemeral::{Project, Dir};
//!
//! fn main() {
//!     let project = Project::new("tmp")
//!        .add_dir(Dir::new("tmp/foo").add_file("bar", &vec![101u8]))
//!        .build();
//!
//!     project.clear();
//! }
//! ```
//!
//! This will create a new project in a dir called `tmp` which will contain a dir "foo" which will
//! contain a file `bar` with `e` (101u8) written to the file.

use std::fs::{create_dir_all, remove_dir_all, File as FsFile};
use std::{error::Error, io::Write, path::PathBuf};

/// Project represents a project created on the file system at any user-defined location defined by
/// the path parameter to the `new()` function.
///
/// This struct as a builder so directories and files can be added to it. Remember to call `build()`
/// at the end to create the project in the filesystem. The dirs vector will contain all the dirs &
/// subdirs in the project, which are added when the directory is added to the project.

#[derive(Clone, Debug)]
pub struct Project {
    pub path: PathBuf,
    dirs: Vec<Dir>,
}

impl Project {
    /// Creates a new Project at the specified `path`. This will automatically add a "root" directory
    /// to the `dirs` vector.

    pub fn new<T>(path: T) -> Project
    where
        T: Into<PathBuf> + Clone,
    {
        let path = path.into();
        Project {
            dirs: vec![Dir::new(&path)],
            path,
        }
    }

    /// Creates the project in the filesystem. This will create all the directories & files that are
    /// added by using `add_dir()`.
    ///
    /// No function should be chained for this, except for `clear()`.
    ///
    /// Function panics if the directory or file cannot be created or written to.

    pub fn build(self) -> Self {
        self.dirs.iter().for_each(|dir| {
            dir.path.mkdir_p().expect("cannot create directory");

            dir.files.iter().for_each(|file| {
                let mut fs_file = FsFile::create(&file.path).expect("cannot create file");
                fs_file
                    .write_all(&file.contents)
                    .expect("cannot write to the file");
            })
        });

        self
    }

    /// Adds a directory to the chain which will be created when `build()` is called. This accepts
    /// a Dir, with the files already attached to it.
    ///
    /// To add a subdirectory, specify the path from
    /// the project root.
    ///
    /// To add files to the root of a directory, you need to call `add_dir()` and give a path which
    /// matches the project path.

    pub fn add_dir(mut self, directory: Dir) -> Self {
        self.dirs.push(directory);

        self
    }

    /// Deletes the project from the filesystem. This function can be used to clear the project
    /// after running the tests.
    ///
    /// This function panics if a directory cannot be deleted.

    pub fn clear(self) {
        remove_dir_all(&self.dirs[0].path).expect("can't delete directory")
    }
}

/// Represents a dir in the filesystem. Accepts a path and contains a vector of files added.
///
/// To a Dir, you can attach files but not other dirs. To attach subdirectories, add them
/// directly to Project and specify the parent dir in the path.

#[derive(Clone, Debug)]
pub struct Dir {
    pub path: PathBuf,
    files: Vec<File>,
}

impl Dir {
    pub fn new<T: Into<PathBuf>>(path: T) -> Dir {
        Dir {
            path: path.into(),
            files: vec![],
        }
    }

    /// Adds a file to the Dir. Accepts any type that can be converted to a PathBuf just like the
    /// rest of the crate. Contents of the file should be specified as well (in bytes).

    pub fn add_file<T: Into<PathBuf>>(mut self, path: T, contents: &[u8]) -> Self {
        let path = path.into();
        let full_path = if path.is_relative() {
            self.path.join(path)
        } else {
            path
        };

        self.files.push(File::new(full_path, contents));

        self
    }
}

impl AsMut<Dir> for Dir {
    fn as_mut(&mut self) -> &mut Dir {
        self
    }
}

/// Represents a file stored in the filesystem. Contains the path and the contents in bytes.

#[derive(Clone, Debug)]
pub struct File {
    pub path: PathBuf,
    contents: Vec<u8>,
}

impl File {
    pub fn new<T: Into<PathBuf>>(path: T, contents: &[u8]) -> File {
        File {
            path: path.into(),
            contents: contents.into(),
        }
    }
}

/// Adds common path-based function. This allows a path-based type to create directories. mkdir_p
/// will recursively create a directory and all of its parent components if they are missing while
/// mkdir will create a single directory.

trait FilePath {
    fn mkdir_p(&self) -> Result<(), Box<dyn Error>>;
}

impl FilePath for PathBuf {
    fn mkdir_p(&self) -> Result<(), Box<dyn Error>> {
        create_dir_all(self).map_err(|err| err.into())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn project_empty_build_creates_dir() {
        let path = PathBuf::from("tmp");
        let project = Project::new(&path);
        project.clone().build();
        assert!(path.exists());
        project.clear();
    }

    #[test]
    fn project_with_dir_and_files_works() {
        let path = PathBuf::from("tmp2");
        let project = Project::new(&path)
            .add_dir(Dir::new("tmp2/foo").add_file("bar", &vec![101u8]))
            .build();

        assert!(path.exists());
        let path = path.join("foo");
        assert!(path.exists());
        let path = path.join("bar");
        assert!(path.exists());
        project.clear();
    }

    #[test]
    fn project_with_1_file_in_root() {
        let path = PathBuf::from("tmp3");
        let project = Project::new(&path)
            .add_dir(Dir::new("tmp3").add_file("bar", b"groot"))
            .build();

        assert!(path.exists());
        let path = path.join("bar");
        assert!(path.exists());

        project.clear();
    }

}