use crate::ViewError;
use std::sync::Arc;
use tera::Tera;
use tokio::sync::RwLock;
#[derive(Debug, Clone)]
pub struct TemplatesConfig {
pub glob: String,
pub auto_reload: bool,
pub strict_mode: bool,
}
impl Default for TemplatesConfig {
fn default() -> Self {
Self {
glob: "templates/**/*.html".to_string(),
auto_reload: cfg!(debug_assertions),
strict_mode: false,
}
}
}
impl TemplatesConfig {
pub fn new(glob: impl Into<String>) -> Self {
Self {
glob: glob.into(),
..Default::default()
}
}
pub fn auto_reload(mut self, enabled: bool) -> Self {
self.auto_reload = enabled;
self
}
pub fn strict_mode(mut self, enabled: bool) -> Self {
self.strict_mode = enabled;
self
}
}
#[derive(Clone)]
pub struct Templates {
inner: Arc<RwLock<Tera>>,
config: TemplatesConfig,
}
impl Templates {
pub fn new(glob: impl Into<String>) -> Result<Self, ViewError> {
let config = TemplatesConfig::new(glob);
Self::with_config(config)
}
pub fn with_config(config: TemplatesConfig) -> Result<Self, ViewError> {
let mut tera = Tera::new(&config.glob)?;
register_builtin_filters(&mut tera);
Ok(Self {
inner: Arc::new(RwLock::new(tera)),
config,
})
}
pub fn empty() -> Self {
Self {
inner: Arc::new(RwLock::new(Tera::default())),
config: TemplatesConfig::default(),
}
}
pub async fn add_template(
&self,
name: impl Into<String>,
content: impl Into<String>,
) -> Result<(), ViewError> {
let mut tera = self.inner.write().await;
tera.add_raw_template(&name.into(), &content.into())?;
Ok(())
}
pub async fn render(
&self,
template: &str,
context: &tera::Context,
) -> Result<String, ViewError> {
#[cfg(debug_assertions)]
if self.config.auto_reload {
let mut tera = self.inner.write().await;
if let Err(e) = tera.full_reload() {
tracing::warn!("Template reload failed: {}", e);
}
}
let tera = self.inner.read().await;
tera.render(template, context).map_err(ViewError::from)
}
pub async fn render_with<T: serde::Serialize>(
&self,
template: &str,
data: &T,
) -> Result<String, ViewError> {
let context = tera::Context::from_serialize(data)
.map_err(|e| ViewError::serialization_error(e.to_string()))?;
self.render(template, &context).await
}
pub async fn has_template(&self, name: &str) -> bool {
let tera = self.inner.read().await;
let result = tera.get_template_names().any(|n| n == name);
result
}
pub async fn template_names(&self) -> Vec<String> {
let tera = self.inner.read().await;
tera.get_template_names().map(String::from).collect()
}
pub async fn reload(&self) -> Result<(), ViewError> {
let mut tera = self.inner.write().await;
tera.full_reload()?;
Ok(())
}
pub fn config(&self) -> &TemplatesConfig {
&self.config
}
}
fn register_builtin_filters(tera: &mut Tera) {
tera.register_filter(
"json_pretty",
|value: &tera::Value, _: &std::collections::HashMap<String, tera::Value>| {
serde_json::to_string_pretty(value)
.map(tera::Value::String)
.map_err(|e| tera::Error::msg(e.to_string()))
},
);
tera.register_filter(
"truncate_words",
|value: &tera::Value, args: &std::collections::HashMap<String, tera::Value>| {
let s = tera::try_get_value!("truncate_words", "value", String, value);
let length = match args.get("length") {
Some(val) => tera::try_get_value!("truncate_words", "length", usize, val),
None => 50,
};
let end = match args.get("end") {
Some(val) => tera::try_get_value!("truncate_words", "end", String, val),
None => "...".to_string(),
};
let words: Vec<&str> = s.split_whitespace().collect();
if words.len() <= length {
Ok(tera::Value::String(s))
} else {
let truncated: String = words[..length].join(" ");
Ok(tera::Value::String(format!("{}{}", truncated, end)))
}
},
);
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_empty_templates() {
let templates = Templates::empty();
templates
.add_template("test", "Hello, {{ name }}!")
.await
.unwrap();
let mut ctx = tera::Context::new();
ctx.insert("name", "World");
let result = templates.render("test", &ctx).await.unwrap();
assert_eq!(result, "Hello, World!");
}
#[tokio::test]
async fn test_render_with_struct() {
#[derive(serde::Serialize)]
struct Data {
name: String,
}
let templates = Templates::empty();
templates
.add_template("test", "Hello, {{ name }}!")
.await
.unwrap();
let data = Data {
name: "Alice".to_string(),
};
let result = templates.render_with("test", &data).await.unwrap();
assert_eq!(result, "Hello, Alice!");
}
}