use std::fs;
use std::path::{Path, PathBuf};
use crate::ProjectType;
use super::{Result, TemplateError, TemplateVariant};
pub struct TemplateLoader {
base_path: PathBuf,
}
impl TemplateLoader {
pub fn new<P: AsRef<Path>>(base_path: P) -> Self {
Self {
base_path: base_path.as_ref().to_path_buf(),
}
}
pub fn load_template(&self, template_path: &str) -> Result<String> {
let full_path = self.base_path.join(template_path);
fs::read_to_string(&full_path).map_err(|e| TemplateError::LoadError {
path: template_path.to_string(),
source: e,
})
}
pub fn template_exists(&self, template_path: &str) -> bool {
let full_path = self.base_path.join(template_path);
full_path.exists()
}
pub fn list_templates(
&self,
project_type: ProjectType,
variant: TemplateVariant,
) -> Result<Vec<PathBuf>> {
let type_dir = match project_type {
ProjectType::Binary => "binary",
ProjectType::Library => "library",
};
let variant_dir = match variant {
TemplateVariant::Minimal => "minimal",
TemplateVariant::Extended => "extended",
};
let template_dir = self.base_path.join(type_dir).join(variant_dir);
let base_dir = self.base_path.join("base");
println!("Template directory: {}", template_dir.display());
println!("Base directory: {}", base_dir.display());
if !template_dir.exists() {
println!("Template directory does not exist!");
return Err(TemplateError::TemplateNotFound {
path: template_dir.to_string_lossy().to_string(),
});
}
let mut templates = if base_dir.exists() {
println!("Base directory exists, collecting templates...");
self.collect_templates_from_dir(&base_dir)?
} else {
println!("Base directory does not exist!");
Vec::new()
};
println!("Collecting templates from project type directory...");
let type_templates = self.collect_templates_from_dir(&template_dir)?;
templates.extend(type_templates);
println!("Found {} templates", templates.len());
for template in &templates {
println!(" - {}", template.display());
}
Ok(templates)
}
#[allow(clippy::only_used_in_recursion)]
fn collect_templates_from_dir(&self, dir: &Path) -> Result<Vec<PathBuf>> {
if !dir.exists() {
return Err(TemplateError::TemplateNotFound {
path: dir.to_string_lossy().to_string(),
});
}
let mut templates = Vec::new();
for entry in fs::read_dir(dir).map_err(|e| TemplateError::LoadError {
path: dir.to_string_lossy().to_string(),
source: e,
})? {
let entry = entry.map_err(|e| TemplateError::LoadError {
path: dir.to_string_lossy().to_string(),
source: e,
})?;
let path = entry.path();
if path.is_dir() {
let sub_templates = self.collect_templates_from_dir(&path)?;
templates.extend(sub_templates);
} else {
if let Some(ext) = path.extension() {
if ext == "hbs" {
templates.push(path);
}
}
}
}
Ok(templates)
}
pub fn get_destination_path(&self, template_path: &Path, dest_root: &Path) -> PathBuf {
let rel_path = pathdiff::diff_paths(template_path, &self.base_path)
.unwrap_or_else(|| template_path.to_path_buf());
let rel_path = rel_path
.strip_prefix("base")
.unwrap_or(&rel_path)
.to_path_buf();
let rel_path = if let Some(components) = rel_path.to_str() {
let components: Vec<&str> = components.split('/').collect();
if components.first() == Some(&"library") || components.first() == Some(&"binary") {
if components.len() >= 3 {
let remaining = components[2..].join("/");
PathBuf::from(remaining)
} else {
rel_path
}
} else {
rel_path
}
} else {
rel_path
};
let dest_path = dest_root.join(rel_path);
if let Some(ext) = dest_path.extension() {
if ext == "hbs" {
return dest_path.with_extension("");
}
}
dest_path
}
pub fn base_path(&self) -> &Path {
&self.base_path
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::TempDir;
fn create_test_template_dir() -> TempDir {
if cfg!(miri) {
panic!("Skipping file system test under Miri");
}
let temp_dir = tempfile::tempdir().unwrap();
fs::create_dir_all(temp_dir.path().join("base")).unwrap();
fs::create_dir_all(temp_dir.path().join("binary/minimal/src")).unwrap();
fs::create_dir_all(temp_dir.path().join("binary/extended/src")).unwrap();
fs::create_dir_all(temp_dir.path().join("library/minimal/src")).unwrap();
fs::create_dir_all(temp_dir.path().join("library/extended/src")).unwrap();
let base_readme = temp_dir.path().join("base/README.md.hbs");
let mut file = fs::File::create(&base_readme).unwrap();
writeln!(file, "# {{{{name}}}}\n\n{{{{description}}}}\n").unwrap();
let binary_main = temp_dir.path().join("binary/minimal/src/main.rs.hbs");
let mut file = fs::File::create(&binary_main).unwrap();
writeln!(
file,
"fn main() {{\n println!(\"Hello from {{name}}!\");\n}}"
)
.unwrap();
let library_lib = temp_dir.path().join("library/minimal/src/lib.rs.hbs");
let mut file = fs::File::create(&library_lib).unwrap();
writeln!(
file,
"//! {{name}} library\n\npub fn add(a: i32, b: i32) -> i32 {{\n a + b\n}}"
)
.unwrap();
temp_dir
}
#[test]
fn test_load_template() {
if cfg!(miri) {
eprintln!("Skipping file system test under Miri");
return;
}
let temp_dir = create_test_template_dir();
let loader = TemplateLoader::new(temp_dir.path());
let template_content = loader.load_template("base/README.md.hbs").unwrap();
assert!(template_content.contains("# {{name}}"));
}
#[test]
fn test_template_exists() {
if cfg!(miri) {
eprintln!("Skipping file system test under Miri");
return;
}
let temp_dir = create_test_template_dir();
let loader = TemplateLoader::new(temp_dir.path());
assert!(loader.template_exists("base/README.md.hbs"));
assert!(!loader.template_exists("nonexistent.hbs"));
}
#[test]
fn test_list_templates() {
if cfg!(miri) {
eprintln!("Skipping file system test under Miri");
return;
}
let temp_dir = create_test_template_dir();
let loader = TemplateLoader::new(temp_dir.path());
let templates = loader
.list_templates(ProjectType::Binary, TemplateVariant::Minimal)
.unwrap();
assert!(templates.len() >= 2);
let has_readme = templates
.iter()
.any(|path| path.to_string_lossy().contains("README.md.hbs"));
let has_main = templates
.iter()
.any(|path| path.to_string_lossy().contains("main.rs.hbs"));
assert!(has_readme);
assert!(has_main);
}
#[test]
fn test_get_destination_path() {
if cfg!(miri) {
eprintln!("Skipping file system test under Miri");
return;
}
let temp_dir = create_test_template_dir();
let loader = TemplateLoader::new(temp_dir.path());
let template_path = temp_dir.path().join("base/README.md.hbs");
let dest_root = PathBuf::from("/tmp/my-project");
let dest_path = loader.get_destination_path(&template_path, &dest_root);
assert_eq!(dest_path, PathBuf::from("/tmp/my-project/README.md"));
}
}