use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use crate::error::{Result, TronError};
use glob::glob;
use walkdir::WalkDir;
#[derive(Debug, Clone)]
pub struct TronTemplate {
content: String,
placeholders: HashMap<String, String>,
path: Option<PathBuf>,
}
impl TronTemplate {
pub fn new(content: &str) -> Result<Self> {
let placeholders = Self::extract_placeholders(content)?;
Ok(Self {
content: content.to_string(),
placeholders,
path: None,
})
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let content = fs::read_to_string(&path)?;
let mut template = Self::new(&content)?;
template.path = Some(path.as_ref().to_path_buf());
Ok(template)
}
pub fn from_directory<P: AsRef<Path>>(dir: P) -> Result<Vec<(String, Self)>> {
let mut templates = Vec::new();
let dir_path = dir.as_ref();
if !dir_path.is_dir() {
return Err(TronError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Directory not found: {}", dir_path.display())
)));
}
for entry in WalkDir::new(dir_path) {
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 matches!(ext_str.as_str(), "tron" | "tpl" | "template") {
let template = Self::from_file(path)?;
let name = path.file_name()
.unwrap()
.to_string_lossy()
.to_string();
templates.push((name, template));
}
}
}
}
Ok(templates)
}
pub fn from_glob(pattern: &str) -> Result<Vec<(String, Self)>> {
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() {
let template = Self::from_file(&path)?;
let name = path.file_name()
.unwrap()
.to_string_lossy()
.to_string();
templates.push((name, template));
}
}
Ok(templates)
}
pub fn from_sources(sources: &[&str]) -> Result<Vec<(String, Self)>> {
let mut all_templates = Vec::new();
for source in sources {
let mut templates = if source.ends_with('/') {
Self::from_directory(source)?
} else if source.contains('*') || source.contains('?') {
Self::from_glob(source)?
} else {
let template = Self::from_file(source)?;
let name = Path::new(source)
.file_name()
.unwrap()
.to_string_lossy()
.to_string();
vec![(name, template)]
};
all_templates.append(&mut templates);
}
Ok(all_templates)
}
pub fn path(&self) -> Option<&Path> {
self.path.as_deref()
}
pub fn content(&self) -> &str {
&self.content
}
pub fn placeholder_names(&self) -> Vec<String> {
self.placeholders.keys().cloned().collect()
}
pub fn has_placeholder(&self, name: &str) -> bool {
self.placeholders.contains_key(name)
}
pub fn get(&self, placeholder: &str) -> Option<&str> {
self.placeholders.get(placeholder).map(|s| s.as_str())
}
pub fn set(&mut self, placeholder: &str, value: &str) -> Result<()> {
if !self.placeholders.contains_key(placeholder) {
return Err(TronError::MissingPlaceholder(placeholder.to_string()));
}
self.placeholders.insert(placeholder.to_string(), value.to_string());
Ok(())
}
pub fn set_many<K, V, I>(&mut self, values: I) -> Result<()>
where
K: AsRef<str>,
V: AsRef<str>,
I: IntoIterator<Item = (K, V)>,
{
for (key, value) in values {
self.set(key.as_ref(), value.as_ref())?;
}
Ok(())
}
pub fn clear(&mut self, placeholder: &str) -> Result<()> {
self.set(placeholder, "")
}
pub fn is_complete(&self) -> bool {
self.placeholders.values().all(|v| !v.is_empty())
}
pub fn missing_placeholders(&self) -> Vec<String> {
self.placeholders
.iter()
.filter_map(|(k, v)| if v.is_empty() { Some(k.clone()) } else { None })
.collect()
}
pub fn render(&self) -> Result<String> {
let mut result = self.content.clone();
for (placeholder, value) in &self.placeholders {
let pattern = format!("@[{}]@", placeholder);
if value.is_empty() {
return Err(TronError::MissingPlaceholder(placeholder.clone()));
}
result = result.replace(&pattern, value);
}
Ok(result)
}
pub fn render_partial(&self) -> Result<String> {
let mut result = self.content.clone();
for (placeholder, value) in &self.placeholders {
if !value.is_empty() {
let pattern = format!("@[{}]@", placeholder);
result = result.replace(&pattern, value);
}
}
Ok(result)
}
fn extract_placeholders(content: &str) -> Result<HashMap<String, String>> {
let mut placeholders = HashMap::new();
let pattern = regex::Regex::new(r"@\[([^]]+)\]@").unwrap();
for capture in pattern.captures_iter(content) {
let placeholder = capture.get(1).unwrap().as_str().trim();
if placeholder.is_empty() {
return Err(TronError::InvalidSyntax("Empty placeholder name".to_string()));
}
if placeholder.contains(char::is_whitespace) {
return Err(TronError::InvalidSyntax(
format!("Placeholder name '{}' contains whitespace", placeholder)
));
}
placeholders.insert(placeholder.to_string(), String::new());
}
Ok(placeholders)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_new_template() -> Result<()> {
let template = TronTemplate::new("Hello @[name]@!")?;
assert_eq!(template.content(), "Hello @[name]@!");
assert!(template.has_placeholder("name"));
assert_eq!(template.placeholder_names().len(), 1);
Ok(())
}
#[test]
fn test_set_and_get() -> Result<()> {
let mut template = TronTemplate::new("Hello @[name]@!")?;
assert_eq!(template.get("name"), Some(""));
template.set("name", "World")?;
assert_eq!(template.get("name"), Some("World"));
Ok(())
}
#[test]
fn test_set_many() -> Result<()> {
let mut template = TronTemplate::new("@[greeting]@ @[name]@!")?;
let mut values = HashMap::new();
values.insert("greeting", "Hello");
values.insert("name", "World");
template.set_many(values)?;
assert_eq!(template.get("greeting"), Some("Hello"));
assert_eq!(template.get("name"), Some("World"));
Ok(())
}
#[test]
fn test_render() -> Result<()> {
let mut template = TronTemplate::new("Hello @[name]@!")?;
template.set("name", "World")?;
let result = template.render()?;
assert_eq!(result, "Hello World!");
Ok(())
}
#[test]
fn test_render_partial() -> Result<()> {
let mut template = TronTemplate::new("@[greeting]@ @[name]@!")?;
template.set("greeting", "Hello")?;
let result = template.render_partial()?;
assert_eq!(result, "Hello @[name]@!");
Ok(())
}
#[test]
fn test_missing_placeholder_error() {
let mut template = TronTemplate::new("Hello @[name]@!").unwrap();
let result = template.set("nonexistent", "value");
assert!(matches!(result, Err(TronError::MissingPlaceholder(_))));
}
#[test]
fn test_incomplete_render_error() {
let template = TronTemplate::new("Hello @[name]@!").unwrap();
let result = template.render();
assert!(matches!(result, Err(TronError::MissingPlaceholder(_))));
}
#[test]
fn test_is_complete() -> Result<()> {
let mut template = TronTemplate::new("@[a]@ @[b]@")?;
assert!(!template.is_complete());
template.set("a", "1")?;
assert!(!template.is_complete());
template.set("b", "2")?;
assert!(template.is_complete());
Ok(())
}
#[test]
fn test_missing_placeholders() -> Result<()> {
let mut template = TronTemplate::new("@[a]@ @[b]@ @[c]@")?;
let missing = template.missing_placeholders();
assert_eq!(missing.len(), 3);
template.set("a", "1")?;
let missing = template.missing_placeholders();
assert_eq!(missing.len(), 2);
assert!(!missing.contains(&"a".to_string()));
Ok(())
}
#[test]
fn test_invalid_syntax() {
let result = TronTemplate::new("@[]@");
assert!(result.is_ok());
let result = TronTemplate::new("@[invalid name]@");
assert!(matches!(result, Err(TronError::InvalidSyntax(_))));
let result = TronTemplate::new("@[valid_name]@");
assert!(result.is_ok());
let result = TronTemplate::new("@[ ]@"); assert!(matches!(result, Err(TronError::InvalidSyntax(_))));
}
#[test]
fn test_complex_template() -> Result<()> {
let mut template = TronTemplate::new(
"fn @[name]@(@[params]@) -> @[return_type]@ {\n @[body]@\n}"
)?;
template.set("name", "add")?;
template.set("params", "a: i32, b: i32")?;
template.set("return_type", "i32")?;
template.set("body", "a + b")?;
let result = template.render()?;
let expected = "fn add(a: i32, b: i32) -> i32 {\n a + b\n}";
assert_eq!(result, expected);
Ok(())
}
#[test]
#[cfg(test)]
fn test_from_directory() -> Result<()> {
let templates = TronTemplate::from_directory("templates")?;
assert!(!templates.is_empty());
let template_names: Vec<&String> = templates.iter().map(|(name, _)| name).collect();
assert!(template_names.iter().any(|&name| name.contains("rust_function")));
Ok(())
}
#[test]
#[cfg(test)]
fn test_from_glob() -> Result<()> {
let templates = TronTemplate::from_glob("templates/*.tron")?;
assert!(!templates.is_empty());
for (name, _template) in &templates {
assert!(name.ends_with(".tron"));
}
Ok(())
}
#[test]
#[cfg(test)]
fn test_from_sources() -> Result<()> {
let templates = TronTemplate::from_sources(&[
"templates/", "templates/*.tpl", ])?;
assert!(!templates.is_empty());
let has_tron = templates.iter().any(|(name, _)| name.ends_with(".tron"));
let has_tpl = templates.iter().any(|(name, _)| name.ends_with(".tpl"));
assert!(has_tron); assert!(has_tpl);
Ok(())
}
#[test]
fn test_invalid_directory() {
let result = TronTemplate::from_directory("nonexistent_directory");
assert!(matches!(result, Err(TronError::Io(_))));
}
#[test]
fn test_invalid_glob_pattern() {
let result = TronTemplate::from_glob("[invalid");
assert!(matches!(result, Err(TronError::Io(_))));
}
}