quickstart_lib/template/
loader.rs

1//! Template loader implementation
2//!
3//! This module provides functionality for loading templates from the filesystem
4//! and managing template paths.
5
6use std::fs;
7use std::path::{Path, PathBuf};
8
9use crate::ProjectType;
10
11use super::{Result, TemplateError, TemplateVariant};
12
13/// Template loader for file-based templates
14pub struct TemplateLoader {
15    /// Base directory for templates
16    base_path: PathBuf,
17}
18
19impl TemplateLoader {
20    /// Create a new template loader with the given base path
21    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    /// Load a template from the filesystem
28    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    /// Check if a template exists at the given path
37    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    /// List all templates applicable for a project type and variant
43    pub fn list_templates(
44        &self,
45        project_type: ProjectType,
46        variant: TemplateVariant,
47    ) -> Result<Vec<PathBuf>> {
48        // Build the directory path for this project type and variant
49        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        // Return error if template directory doesn't exist
66        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        // Collect templates from base directory if it exists
74        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        // Collect templates from project type directory
83        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    /// Recursively collect templates from a directory
96    #[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                // Recursively collect templates from subdirectories
119                let sub_templates = self.collect_templates_from_dir(&path)?;
120                templates.extend(sub_templates);
121            } else {
122                // Only include files with .hbs extension
123                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    /// Get the destination path for a template
135    pub fn get_destination_path(&self, template_path: &Path, dest_root: &Path) -> PathBuf {
136        // Calculate the relative path from base_path to template_path
137        let rel_path = pathdiff::diff_paths(template_path, &self.base_path)
138            .unwrap_or_else(|| template_path.to_path_buf());
139
140        // If the template is from the 'base/' directory, strip 'base/' from the path
141        let rel_path = rel_path
142            .strip_prefix("base")
143            .unwrap_or(&rel_path)
144            .to_path_buf();
145
146        // If the template is from a project type directory, strip the project type and variant
147        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                    // Skip project type and variant directories
152                    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        // Join with destination root to get final path
165        let dest_path = dest_root.join(rel_path);
166
167        // For template files (.hbs extension), remove the extension
168        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    /// Get the base path of the template loader
178    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        // Skip under Miri
191        if cfg!(miri) {
192            panic!("Skipping file system test under Miri");
193        }
194
195        let temp_dir = tempfile::tempdir().unwrap();
196
197        // Create base directory
198        fs::create_dir_all(temp_dir.path().join("base")).unwrap();
199
200        // Create binary directory structure
201        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        // Create library directory structure
205        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        // Create some test template files
209        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        // Skip under Miri
235        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        // Skip under Miri
250        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        // Skip under Miri
265        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        // Should find at least the base README and binary main.rs
278        assert!(templates.len() >= 2);
279
280        // Check that the expected templates are included
281        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        // Skip under Miri
295        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        // Should be /tmp/my-project/README.md (without .hbs extension)
309        assert_eq!(dest_path, PathBuf::from("/tmp/my-project/README.md"));
310    }
311}