pipi/generator/executer/
filesystem.rs1use 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, ©_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, ©_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 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, ©_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, ©_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}