Skip to main content

pipi/generator/executer/
filesystem.rs

1use std::path::{Path, PathBuf};
2
3use fs_extra::file::{move_file, write_all};
4use walkdir::WalkDir;
5
6use super::Executer;
7use crate::{generator, settings::Settings};
8
9#[derive(Debug, Default, Clone)]
10pub struct FileSystem {
11    pub source_dir: PathBuf,
12    pub target_dir: PathBuf,
13    pub template_engine: generator::template::Template,
14}
15
16impl FileSystem {
17    #[must_use]
18    pub fn new(from: &Path, to: &Path) -> Self {
19        Self {
20            source_dir: from.to_path_buf(),
21            target_dir: to.to_path_buf(),
22            template_engine: generator::template::Template::default(),
23        }
24    }
25
26    #[must_use]
27    pub fn with_template_engine(
28        from: &Path,
29        to: &Path,
30        template_engine: generator::template::Template,
31    ) -> Self {
32        Self {
33            source_dir: from.to_path_buf(),
34            target_dir: to.to_path_buf(),
35            template_engine,
36        }
37    }
38
39    fn render_and_rename_template_file(
40        &self,
41        file_path: &Path,
42        settings: &Settings,
43    ) -> super::Result<()> {
44        let template_content = fs_extra::file::read_to_string(file_path).map_err(|err| {
45            tracing::debug!(err = %err, "failed to read template file");
46            err
47        })?;
48        let rendered_content = self.template_engine.render(&template_content, settings)?;
49        write_all(file_path, &rendered_content).map_err(|err| {
50            tracing::debug!(err = %err, "failed to write rendered content to file");
51            err
52        })?;
53
54        let renamed_path = self
55            .template_engine
56            .strip_template_extension(file_path)
57            .map_err(|err| {
58                tracing::debug!(err = %err, "error stripping template extension from file");
59                super::Error::msg("error striping template file")
60            })?;
61        move_file(file_path, renamed_path, &fs_extra::file::CopyOptions::new())?;
62        Ok(())
63    }
64}
65
66impl Executer for FileSystem {
67    fn copy_file(&self, path: &Path) -> super::Result<PathBuf> {
68        let source_path = self.source_dir.join(path);
69        let target_path = self.target_dir.join(path);
70
71        let span = tracing::error_span!("copy_file", source_path = %source_path.display(), target_path = %target_path.display());
72        let _guard = span.enter();
73
74        tracing::debug!("starting file copy operation");
75
76        fs_extra::dir::create_all(target_path.parent().unwrap(), false).map_err(|error| {
77            tracing::debug!(error = %error, "error creating target parent directory");
78            error
79        })?;
80
81        let copy_options = fs_extra::file::CopyOptions::new();
82        fs_extra::file::copy(source_path, &target_path, &copy_options)?;
83        tracing::debug!("file copy completed successfully");
84
85        Ok(target_path)
86    }
87
88    fn create_file(&self, path: &Path, content: String) -> super::Result<PathBuf> {
89        let target_path = self.target_dir.join(path);
90        if let Some(parent) = path.parent() {
91            fs_extra::dir::create_all(self.target_dir.join(parent), false)?;
92        }
93
94        let span = tracing::info_span!("create_file", target_path = %target_path.display());
95        let _guard = span.enter();
96
97        tracing::debug!("starting file copy operation");
98
99        fs_extra::dir::create_all(target_path.parent().unwrap(), false).map_err(|error| {
100            tracing::debug!(error = %error, "error creating target parent directory");
101            error
102        })?;
103
104        fs_extra::file::write_all(&target_path, &content)?;
105        tracing::debug!("file created successfully");
106
107        Ok(target_path)
108    }
109
110    fn copy_dir(&self, directory_path: &Path) -> super::Result<()> {
111        let source_path = self.source_dir.join(directory_path);
112        let target_path = self.target_dir.join(directory_path);
113
114        let span = tracing::error_span!("", source_path = %source_path.display(), target_path = %target_path.display());
115        let _guard = span.enter();
116
117        tracing::debug!("starting directory copy operation");
118        let copy_options = fs_extra::dir::CopyOptions::new().copy_inside(true);
119        fs_extra::dir::copy(source_path, target_path, &copy_options)?;
120        tracing::debug!("directory copy completed successfully");
121        Ok(())
122    }
123
124    fn copy_template(&self, file_path: &Path, settings: &Settings) -> super::Result<()> {
125        let span = tracing::error_span!("copy_template", file_path = %file_path.display());
126        let _guard: tracing::span::Entered<'_> = span.enter();
127        if !self.template_engine.is_template(file_path) {
128            tracing::debug!("file is not a template, skipping rendering");
129            return Err(super::Error::msg("File is not a template"));
130        }
131
132        //todo fix the if here
133        tracing::debug!("copying template file");
134
135        let copied_path = self.copy_file(file_path)?;
136        self.render_and_rename_template_file(&copied_path, settings)
137    }
138
139    #[allow(clippy::cognitive_complexity)]
140    fn copy_template_dir(&self, directory_path: &Path, settings: &Settings) -> super::Result<()> {
141        let source_path = self.source_dir.join(directory_path);
142        let target_path = self.target_dir.join(directory_path);
143
144        let span = tracing::error_span!("copy_template_dir", source_path = %source_path.display(), target_path = %target_path.display());
145        let _guard: tracing::span::Entered<'_> = span.enter();
146
147        tracing::debug!("starting template directory copy operation");
148
149        let copy_options = fs_extra::dir::CopyOptions::new().copy_inside(true);
150        fs_extra::dir::copy(source_path, target_path, &copy_options)?;
151
152        tracing::debug!("scanning copied directory for template files to render");
153        for entry in WalkDir::new(self.target_dir.join(directory_path))
154            .into_iter()
155            .filter_map(Result::ok)
156        {
157            let path = entry.path();
158            if self.template_engine.is_template(path) {
159                tracing::debug!(template_path = %path.display(), "rendering template file in directory");
160                self.render_and_rename_template_file(path, settings)?;
161            } else {
162                tracing::debug!(file_path = %path.display(), "not a template file");
163            }
164        }
165
166        Ok(())
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use tree_fs::TreeBuilder;
173
174    use super::*;
175
176    fn init_filesystem() -> (FileSystem, tree_fs::Tree) {
177        let tree_fs = TreeBuilder::default()
178            .add("test/foo.txt", "bar")
179            .add("test/bar.txt.t", "crate: {{settings.package_name}}")
180            .create()
181            .expect("Failed to create mock data");
182
183        let copy_to = TreeBuilder::default()
184            .create()
185            .expect("Failed to create mock data");
186        (FileSystem::new(&tree_fs.root, &copy_to.root), tree_fs)
187    }
188
189    #[test]
190    fn can_copy_file() {
191        let (fs, _tree_fs) = init_filesystem();
192
193        assert!(fs.copy_file(&PathBuf::from("test").join("foo.txt")).is_ok());
194        let copied_path = fs.target_dir.join("test").join("foo.txt");
195        assert!(copied_path.exists());
196        assert_eq!(
197            fs_extra::file::read_to_string(copied_path).expect("read content"),
198            "bar"
199        );
200    }
201
202    #[test]
203    fn can_copy_dir() {
204        let (fs, _tree_fs) = init_filesystem();
205        assert!(fs.copy_dir(&PathBuf::from("test")).is_ok());
206        let copied_path_1 = fs.target_dir.join("test").join("foo.txt");
207        let copied_path_2 = fs.target_dir.join("test").join("bar.txt.t");
208        assert!(copied_path_1.exists());
209        assert!(copied_path_2.exists());
210
211        assert_eq!(
212            fs_extra::file::read_to_string(copied_path_1).expect("read content"),
213            "bar"
214        );
215
216        assert_eq!(
217            fs_extra::file::read_to_string(copied_path_2).expect("read content"),
218            "crate: {{settings.package_name}}"
219        );
220    }
221
222    #[test]
223    fn can_copy_template() {
224        let (fs, _tree_fs) = init_filesystem();
225
226        let settings = Settings {
227            package_name: "pipi-app".to_string(),
228            ..Default::default()
229        };
230
231        assert!(fs
232            .copy_template(&PathBuf::from("test").join("bar.txt.t"), &settings)
233            .is_ok());
234        let copied_path = fs.target_dir.join("test").join("bar.txt");
235        assert!(copied_path.exists());
236        assert_eq!(
237            fs_extra::file::read_to_string(copied_path).expect("read content"),
238            "crate: pipi-app"
239        );
240    }
241
242    #[test]
243    fn can_copy_template_dir() {
244        let (fs, _tree_fs) = init_filesystem();
245
246        let settings = Settings {
247            package_name: "pipi-app".to_string(),
248            ..Default::default()
249        };
250
251        assert!(fs
252            .copy_template_dir(&PathBuf::from("test"), &settings)
253            .is_ok());
254        let copied_path_1 = fs.target_dir.join("test").join("foo.txt");
255        let copied_path_2 = fs.target_dir.join("test").join("bar.txt");
256        assert!(copied_path_1.exists());
257        assert!(copied_path_2.exists());
258
259        assert_eq!(
260            fs_extra::file::read_to_string(copied_path_1).expect("read content"),
261            "bar"
262        );
263
264        assert_eq!(
265            fs_extra::file::read_to_string(copied_path_2).expect("read content"),
266            "crate: pipi-app"
267        );
268    }
269}