use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use crate::error::{Result, TronError};
use crate::template::TronTemplate;
use crate::cache::{TemplateCache, CacheConfig};
use walkdir::WalkDir;
use glob::glob;
#[derive(Debug, Clone)]
pub struct LoaderConfig {
pub recursive: bool,
pub extensions: Vec<String>,
pub track_changes: bool,
pub max_depth: Option<usize>,
pub enable_caching: bool,
pub cache_config: CacheConfig,
}
impl Default for LoaderConfig {
fn default() -> Self {
Self {
recursive: true,
extensions: vec!["tron".to_string(), "tpl".to_string(), "template".to_string()],
track_changes: false,
max_depth: None,
enable_caching: true,
cache_config: CacheConfig::default(),
}
}
}
#[derive(Debug, Clone)]
pub struct TemplateMetadata {
pub path: PathBuf,
pub modified: Option<SystemTime>,
pub size: u64,
}
pub struct TemplateLoader {
config: LoaderConfig,
cache: HashMap<PathBuf, (TronTemplate, TemplateMetadata)>,
template_cache: Option<TemplateCache>,
}
impl TemplateLoader {
pub fn new() -> Self {
let config = LoaderConfig::default();
let template_cache = if config.enable_caching {
Some(TemplateCache::with_config(config.cache_config.clone()))
} else {
None
};
Self {
config,
cache: HashMap::new(),
template_cache,
}
}
pub fn with_config(config: LoaderConfig) -> Self {
let template_cache = if config.enable_caching {
Some(TemplateCache::with_config(config.cache_config.clone()))
} else {
None
};
Self {
config,
cache: HashMap::new(),
template_cache,
}
}
pub fn load_from_directory<P: AsRef<Path>>(&mut self, dir: P) -> Result<Vec<(String, TronTemplate)>> {
let dir_path = dir.as_ref();
let mut templates = Vec::new();
if !dir_path.is_dir() {
return Err(TronError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Directory not found: {}", dir_path.display())
)));
}
let walker = if self.config.recursive {
WalkDir::new(dir_path)
} else {
WalkDir::new(dir_path).max_depth(1)
};
let walker = if let Some(max_depth) = self.config.max_depth {
walker.max_depth(max_depth)
} else {
walker
};
for entry in walker {
let entry = entry.map_err(|e| TronError::Io(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to walk directory: {}", e)
)))?;
let path = entry.path();
if path.is_file() {
if let Some(extension) = path.extension() {
let ext_str = extension.to_string_lossy().to_lowercase();
if self.config.extensions.contains(&ext_str) {
let template = self.load_template(path)?;
let name = path.file_name()
.unwrap()
.to_string_lossy()
.to_string();
templates.push((name, template));
}
}
}
}
Ok(templates)
}
pub fn load_from_glob(&mut self, pattern: &str) -> Result<Vec<(String, TronTemplate)>> {
let mut templates = Vec::new();
let glob_result = glob(pattern)
.map_err(|e| TronError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Invalid glob pattern '{}': {}", pattern, e)
)))?;
for entry in glob_result {
let path = entry.map_err(|e| TronError::Io(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Glob error: {}", e)
)))?;
if path.is_file() {
if let Some(extension) = path.extension() {
let ext_str = extension.to_string_lossy().to_lowercase();
if self.config.extensions.contains(&ext_str) {
let template = self.load_template(&path)?;
let name = path.file_name()
.unwrap()
.to_string_lossy()
.to_string();
templates.push((name, template));
}
}
}
}
Ok(templates)
}
pub fn discover_templates<P: AsRef<Path>>(&self, dir: P) -> Result<Vec<(String, TemplateMetadata)>> {
let dir_path = dir.as_ref();
let mut templates = Vec::new();
if !dir_path.is_dir() {
return Err(TronError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Directory not found: {}", dir_path.display())
)));
}
let walker = if self.config.recursive {
WalkDir::new(dir_path)
} else {
WalkDir::new(dir_path).max_depth(1)
};
let walker = if let Some(max_depth) = self.config.max_depth {
walker.max_depth(max_depth)
} else {
walker
};
for entry in walker {
let entry = entry.map_err(|e| TronError::Io(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to walk directory: {}", e)
)))?;
let path = entry.path();
if path.is_file() {
if let Some(extension) = path.extension() {
let ext_str = extension.to_string_lossy().to_lowercase();
if self.config.extensions.contains(&ext_str) {
let metadata = std::fs::metadata(path)?;
let template_metadata = TemplateMetadata {
path: path.to_path_buf(),
modified: metadata.modified().ok(),
size: metadata.len(),
};
let name = path.file_name()
.unwrap()
.to_string_lossy()
.to_string();
templates.push((name, template_metadata));
}
}
}
}
Ok(templates)
}
fn load_template<P: AsRef<Path>>(&mut self, path: P) -> Result<TronTemplate> {
let path = path.as_ref();
if let Some(ref template_cache) = self.template_cache {
if let Some(cached_template) = template_cache.get_by_path(path) {
return Ok(cached_template);
}
}
if self.config.track_changes {
if let Some((cached_template, metadata)) = self.cache.get(path) {
if let Ok(file_metadata) = std::fs::metadata(path) {
if let (Some(cached_modified), Ok(current_modified)) =
(metadata.modified, file_metadata.modified()) {
if current_modified <= cached_modified {
return Ok(cached_template.clone());
}
}
}
}
}
let template = TronTemplate::from_file(path)?;
if let Some(ref template_cache) = self.template_cache {
template_cache.insert_template(template.clone())?;
}
if self.config.track_changes {
if let Ok(file_metadata) = std::fs::metadata(path) {
let template_metadata = TemplateMetadata {
path: path.to_path_buf(),
modified: file_metadata.modified().ok(),
size: file_metadata.len(),
};
self.cache.insert(path.to_path_buf(), (template.clone(), template_metadata));
}
}
Ok(template)
}
pub fn clear_cache(&mut self) {
self.cache.clear();
if let Some(ref template_cache) = self.template_cache {
template_cache.clear();
}
}
pub fn cache_size(&self) -> usize {
if let Some(ref template_cache) = self.template_cache {
template_cache.size()
} else {
self.cache.len()
}
}
pub fn is_cached<P: AsRef<Path>>(&self, path: P) -> bool {
if let Some(ref template_cache) = self.template_cache {
template_cache.is_cached(path.as_ref())
} else {
self.cache.contains_key(path.as_ref())
}
}
pub fn cache_stats(&self) -> Option<crate::cache::CacheStats> {
if let Some(ref template_cache) = self.template_cache {
template_cache.stats()
} else {
None
}
}
pub fn cleanup_expired_cache(&self) {
if let Some(ref template_cache) = self.template_cache {
template_cache.cleanup_expired();
}
}
}
impl Default for TemplateLoader {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_loader_config_default() {
let config = LoaderConfig::default();
assert!(config.recursive);
assert_eq!(config.extensions, vec!["tron", "tpl", "template"]);
assert!(!config.track_changes);
assert_eq!(config.max_depth, None);
}
#[test]
fn test_template_loader_new() {
let loader = TemplateLoader::new();
assert_eq!(loader.cache_size(), 0);
assert!(loader.config.recursive);
}
#[test]
#[cfg(test)]
fn test_load_from_directory() -> Result<()> {
let mut loader = TemplateLoader::new();
let templates = loader.load_from_directory("templates")?;
assert!(!templates.is_empty());
Ok(())
}
#[test]
#[cfg(test)]
fn test_discover_templates() -> Result<()> {
let loader = TemplateLoader::new();
let discovered = loader.discover_templates("templates")?;
assert!(!discovered.is_empty());
for (_name, metadata) in discovered {
assert!(metadata.path.exists());
assert!(metadata.size > 0);
}
Ok(())
}
#[test]
fn test_loader_with_config() {
let config = LoaderConfig {
recursive: false,
extensions: vec!["tron".to_string()],
track_changes: true,
max_depth: Some(1),
enable_caching: true,
cache_config: CacheConfig::default(),
};
let loader = TemplateLoader::with_config(config);
assert!(!loader.config.recursive);
assert_eq!(loader.config.extensions, vec!["tron"]);
assert!(loader.config.track_changes);
assert_eq!(loader.config.max_depth, Some(1));
assert!(loader.config.enable_caching);
assert!(loader.template_cache.is_some());
}
#[test]
fn test_cache_operations() {
let mut loader = TemplateLoader::new();
assert_eq!(loader.cache_size(), 0);
loader.clear_cache();
assert_eq!(loader.cache_size(), 0);
assert!(!loader.is_cached("nonexistent.tron"));
}
}