use crate::context::TemplateContext;
use crate::error::{Result, TemplateError};
use crate::functions::{register_functions, TimestampProvider};
use std::path::Path;
use std::sync::OnceLock;
use tera::Tera;
#[derive(Clone)]
pub struct TemplateRenderer {
pub(crate) tera: Tera,
context: TemplateContext,
determinism: Option<std::sync::Arc<dyn TimestampProvider + Send + Sync>>,
}
impl TemplateRenderer {
pub fn new() -> Result<Self> {
let mut tera = Tera::default();
register_functions(&mut tera, None)?;
crate::functions::extended::register_extended_functions(&mut tera);
tera.add_raw_template("_macros.toml.tera", crate::MACRO_LIBRARY)
.map_err(|e| {
TemplateError::RenderError(format!("Failed to load macro library: {}", e))
})?;
Ok(Self {
tera,
context: TemplateContext::new(),
determinism: None,
})
}
pub fn with_defaults() -> Result<Self> {
let mut tera = Tera::default();
register_functions(&mut tera, None)?;
crate::functions::extended::register_extended_functions(&mut tera);
tera.add_raw_template("_macros.toml.tera", crate::MACRO_LIBRARY)
.map_err(|e| {
TemplateError::RenderError(format!("Failed to load macro library: {}", e))
})?;
Ok(Self {
tera,
context: TemplateContext::with_defaults(),
determinism: None,
})
}
pub fn with_context(mut self, context: TemplateContext) -> Self {
self.context = context;
self
}
pub fn with_determinism(
mut self,
determinism: std::sync::Arc<dyn TimestampProvider + Send + Sync>,
) -> Self {
self.determinism = Some(determinism);
self
}
pub fn merge_user_vars(
&mut self,
user_vars: std::collections::HashMap<String, serde_json::Value>,
) {
self.context.merge_user_vars(user_vars);
}
pub fn render_file(&mut self, path: &Path) -> Result<String> {
let template_str = std::fs::read_to_string(path)
.map_err(|e| TemplateError::IoError(format!("Failed to read template: {}", e)))?;
let path_str = path.to_str().ok_or_else(|| {
TemplateError::ValidationError(format!(
"Template path contains invalid UTF-8 characters: {}",
path.display()
))
})?;
self.render_str(&template_str, path_str)
}
pub fn render_str(&mut self, template: &str, name: &str) -> Result<String> {
let tera_ctx = self.context.to_tera_context()?;
self.tera.render_str(template, &tera_ctx).map_err(|e| {
TemplateError::RenderError(format!("Template rendering failed in '{}': {}", name, e))
})
}
pub fn render_to_format(
&mut self,
template: &str,
name: &str,
format: OutputFormat,
) -> Result<String> {
let rendered = self.render_str(template, name)?;
match format {
OutputFormat::Toml => Ok(rendered),
OutputFormat::Json => crate::simple::convert_to_json(&rendered),
OutputFormat::Yaml => crate::simple::convert_to_yaml(&rendered),
OutputFormat::Plain => crate::simple::strip_template_syntax(&rendered),
}
}
pub fn render_template_string(&mut self, template: &str, name: &str) -> Result<String> {
self.tera.add_raw_template(name, template).map_err(|e| {
TemplateError::RenderError(format!("Failed to add template '{}': {}", name, e))
})?;
self.tera.render(name, &tera::Context::new()).map_err(|e| {
TemplateError::RenderError(format!("Failed to render template '{}': {}", name, e))
})
}
pub fn render_from_glob(&mut self, glob_pattern: &str, template_name: &str) -> Result<String> {
self.tera
.add_template_file(glob_pattern, Some(template_name))
.map_err(|e| {
TemplateError::RenderError(format!(
"Failed to add templates from glob '{}': {}",
glob_pattern, e
))
})?;
let tera_ctx = self.context.to_tera_context()?;
self.tera.render(template_name, &tera_ctx).map_err(|e| {
TemplateError::RenderError(format!(
"Template rendering failed for '{}': {}",
template_name, e
))
})
}
pub fn enable_inheritance(self) -> Result<Self> {
Ok(self)
}
pub fn add_template(&mut self, name: &str, content: &str) -> Result<()> {
self.tera.add_raw_template(name, content).map_err(|e| {
TemplateError::RenderError(format!("Failed to add template '{}': {}", name, e))
})
}
pub fn template_names(&self) -> Vec<&str> {
self.tera.get_template_names().collect()
}
pub fn has_template(&self, name: &str) -> bool {
self.tera.templates.contains_key(name)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum OutputFormat {
#[default]
Toml,
Json,
Yaml,
Plain,
}
pub fn render_template(
template_content: &str,
user_vars: std::collections::HashMap<String, serde_json::Value>,
) -> Result<String> {
let mut renderer = TemplateRenderer::with_defaults()?;
renderer.merge_user_vars(user_vars);
renderer.render_str(template_content, "template")
}
pub fn render_template_file(
template_path: &Path,
user_vars: std::collections::HashMap<String, serde_json::Value>,
) -> Result<String> {
let template_content = std::fs::read_to_string(template_path)
.map_err(|e| TemplateError::IoError(format!("Failed to read template file: {}", e)))?;
render_template(&template_content, user_vars)
}
pub fn is_template(content: &str) -> bool {
content.contains("{{") || content.contains("{%") || content.contains("{#")
}
pub fn get_cached_template_renderer() -> Result<TemplateRenderer> {
static INSTANCE: OnceLock<Result<TemplateRenderer>> = OnceLock::new();
INSTANCE.get_or_init(TemplateRenderer::new).clone()
}