quickstart_lib/template/
loader.rs1use std::fs;
7use std::path::{Path, PathBuf};
8
9use crate::ProjectType;
10
11use super::{Result, TemplateError, TemplateVariant};
12
13pub struct TemplateLoader {
15 base_path: PathBuf,
17}
18
19impl TemplateLoader {
20 pub fn new<P: AsRef<Path>>(base_path: P) -> Self {
22 Self {
23 base_path: base_path.as_ref().to_path_buf(),
24 }
25 }
26
27 pub fn load_template(&self, template_path: &str) -> Result<String> {
29 let full_path = self.base_path.join(template_path);
30 fs::read_to_string(&full_path).map_err(|e| TemplateError::LoadError {
31 path: template_path.to_string(),
32 source: e,
33 })
34 }
35
36 pub fn template_exists(&self, template_path: &str) -> bool {
38 let full_path = self.base_path.join(template_path);
39 full_path.exists()
40 }
41
42 pub fn list_templates(
44 &self,
45 project_type: ProjectType,
46 variant: TemplateVariant,
47 ) -> Result<Vec<PathBuf>> {
48 let type_dir = match project_type {
50 ProjectType::Binary => "binary",
51 ProjectType::Library => "library",
52 };
53
54 let variant_dir = match variant {
55 TemplateVariant::Minimal => "minimal",
56 TemplateVariant::Extended => "extended",
57 };
58
59 let template_dir = self.base_path.join(type_dir).join(variant_dir);
60 let base_dir = self.base_path.join("base");
61
62 println!("Template directory: {}", template_dir.display());
63 println!("Base directory: {}", base_dir.display());
64
65 if !template_dir.exists() {
67 println!("Template directory does not exist!");
68 return Err(TemplateError::TemplateNotFound {
69 path: template_dir.to_string_lossy().to_string(),
70 });
71 }
72
73 let mut templates = if base_dir.exists() {
75 println!("Base directory exists, collecting templates...");
76 self.collect_templates_from_dir(&base_dir)?
77 } else {
78 println!("Base directory does not exist!");
79 Vec::new()
80 };
81
82 println!("Collecting templates from project type directory...");
84 let type_templates = self.collect_templates_from_dir(&template_dir)?;
85 templates.extend(type_templates);
86
87 println!("Found {} templates", templates.len());
88 for template in &templates {
89 println!(" - {}", template.display());
90 }
91
92 Ok(templates)
93 }
94
95 #[allow(clippy::only_used_in_recursion)]
97 fn collect_templates_from_dir(&self, dir: &Path) -> Result<Vec<PathBuf>> {
98 if !dir.exists() {
99 return Err(TemplateError::TemplateNotFound {
100 path: dir.to_string_lossy().to_string(),
101 });
102 }
103
104 let mut templates = Vec::new();
105
106 for entry in fs::read_dir(dir).map_err(|e| TemplateError::LoadError {
107 path: dir.to_string_lossy().to_string(),
108 source: e,
109 })? {
110 let entry = entry.map_err(|e| TemplateError::LoadError {
111 path: dir.to_string_lossy().to_string(),
112 source: e,
113 })?;
114
115 let path = entry.path();
116
117 if path.is_dir() {
118 let sub_templates = self.collect_templates_from_dir(&path)?;
120 templates.extend(sub_templates);
121 } else {
122 if let Some(ext) = path.extension() {
124 if ext == "hbs" {
125 templates.push(path);
126 }
127 }
128 }
129 }
130
131 Ok(templates)
132 }
133
134 pub fn get_destination_path(&self, template_path: &Path, dest_root: &Path) -> PathBuf {
136 let rel_path = pathdiff::diff_paths(template_path, &self.base_path)
138 .unwrap_or_else(|| template_path.to_path_buf());
139
140 let rel_path = rel_path
142 .strip_prefix("base")
143 .unwrap_or(&rel_path)
144 .to_path_buf();
145
146 let rel_path = if let Some(components) = rel_path.to_str() {
148 let components: Vec<&str> = components.split('/').collect();
149 if components.first() == Some(&"library") || components.first() == Some(&"binary") {
150 if components.len() >= 3 {
151 let remaining = components[2..].join("/");
153 PathBuf::from(remaining)
154 } else {
155 rel_path
156 }
157 } else {
158 rel_path
159 }
160 } else {
161 rel_path
162 };
163
164 let dest_path = dest_root.join(rel_path);
166
167 if let Some(ext) = dest_path.extension() {
169 if ext == "hbs" {
170 return dest_path.with_extension("");
171 }
172 }
173
174 dest_path
175 }
176
177 pub fn base_path(&self) -> &Path {
179 &self.base_path
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use std::io::Write;
187 use tempfile::TempDir;
188
189 fn create_test_template_dir() -> TempDir {
190 if cfg!(miri) {
192 panic!("Skipping file system test under Miri");
193 }
194
195 let temp_dir = tempfile::tempdir().unwrap();
196
197 fs::create_dir_all(temp_dir.path().join("base")).unwrap();
199
200 fs::create_dir_all(temp_dir.path().join("binary/minimal/src")).unwrap();
202 fs::create_dir_all(temp_dir.path().join("binary/extended/src")).unwrap();
203
204 fs::create_dir_all(temp_dir.path().join("library/minimal/src")).unwrap();
206 fs::create_dir_all(temp_dir.path().join("library/extended/src")).unwrap();
207
208 let base_readme = temp_dir.path().join("base/README.md.hbs");
210 let mut file = fs::File::create(&base_readme).unwrap();
211 writeln!(file, "# {{{{name}}}}\n\n{{{{description}}}}\n").unwrap();
212
213 let binary_main = temp_dir.path().join("binary/minimal/src/main.rs.hbs");
214 let mut file = fs::File::create(&binary_main).unwrap();
215 writeln!(
216 file,
217 "fn main() {{\n println!(\"Hello from {{name}}!\");\n}}"
218 )
219 .unwrap();
220
221 let library_lib = temp_dir.path().join("library/minimal/src/lib.rs.hbs");
222 let mut file = fs::File::create(&library_lib).unwrap();
223 writeln!(
224 file,
225 "//! {{name}} library\n\npub fn add(a: i32, b: i32) -> i32 {{\n a + b\n}}"
226 )
227 .unwrap();
228
229 temp_dir
230 }
231
232 #[test]
233 fn test_load_template() {
234 if cfg!(miri) {
236 eprintln!("Skipping file system test under Miri");
237 return;
238 }
239
240 let temp_dir = create_test_template_dir();
241 let loader = TemplateLoader::new(temp_dir.path());
242
243 let template_content = loader.load_template("base/README.md.hbs").unwrap();
244 assert!(template_content.contains("# {{name}}"));
245 }
246
247 #[test]
248 fn test_template_exists() {
249 if cfg!(miri) {
251 eprintln!("Skipping file system test under Miri");
252 return;
253 }
254
255 let temp_dir = create_test_template_dir();
256 let loader = TemplateLoader::new(temp_dir.path());
257
258 assert!(loader.template_exists("base/README.md.hbs"));
259 assert!(!loader.template_exists("nonexistent.hbs"));
260 }
261
262 #[test]
263 fn test_list_templates() {
264 if cfg!(miri) {
266 eprintln!("Skipping file system test under Miri");
267 return;
268 }
269
270 let temp_dir = create_test_template_dir();
271 let loader = TemplateLoader::new(temp_dir.path());
272
273 let templates = loader
274 .list_templates(ProjectType::Binary, TemplateVariant::Minimal)
275 .unwrap();
276
277 assert!(templates.len() >= 2);
279
280 let has_readme = templates
282 .iter()
283 .any(|path| path.to_string_lossy().contains("README.md.hbs"));
284 let has_main = templates
285 .iter()
286 .any(|path| path.to_string_lossy().contains("main.rs.hbs"));
287
288 assert!(has_readme);
289 assert!(has_main);
290 }
291
292 #[test]
293 fn test_get_destination_path() {
294 if cfg!(miri) {
296 eprintln!("Skipping file system test under Miri");
297 return;
298 }
299
300 let temp_dir = create_test_template_dir();
301 let loader = TemplateLoader::new(temp_dir.path());
302
303 let template_path = temp_dir.path().join("base/README.md.hbs");
304 let dest_root = PathBuf::from("/tmp/my-project");
305
306 let dest_path = loader.get_destination_path(&template_path, &dest_root);
307
308 assert_eq!(dest_path, PathBuf::from("/tmp/my-project/README.md"));
310 }
311}