#[cfg(test)]
mod tests;
use std::sync::{OnceLock, RwLock};
pub use tera::Context;
pub use tera::Value;
use crate::core::New;
use crate::mime_type::MimeType;
use crate::range::Range;
use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
static ENGINE: OnceLock<RwLock<TeraEngine>> = OnceLock::new();
#[derive(Debug)]
pub struct TeraEngine {
inner: tera::Tera,
glob: Option<String>,
}
impl TeraEngine {
pub fn from_glob(pattern: &str) -> Result<Self, String> {
let tera = tera::Tera::new(pattern)
.map_err(|e| format!("template engine init failed: {}", e))?;
Ok(TeraEngine { inner: tera, glob: Some(pattern.to_string()) })
}
pub fn from_dir(dir: &str) -> Result<Self, String> {
let pattern = format!("{}/**/*", dir.trim_end_matches('/'));
Self::from_glob(&pattern)
}
pub fn from_raw(templates: &[(&str, &str)]) -> Result<Self, String> {
let mut tera = tera::Tera::default();
for (name, content) in templates {
tera.add_raw_template(name, content)
.map_err(|e| format!("failed to add template '{}': {}", name, e))?;
}
Ok(TeraEngine { inner: tera, glob: None })
}
pub fn render(&self, template_name: &str, ctx: &Context) -> Result<String, String> {
self.inner
.render(template_name, ctx)
.map_err(|e| format!("render '{}' failed: {}", template_name, e))
}
pub fn response(&self, template_name: &str, ctx: &Context) -> Result<Response, String> {
let html = self.render(template_name, ctx)?;
Ok(html_response(html))
}
pub fn reload(&mut self) -> Result<(), String> {
let pattern = self.glob.as_ref().ok_or_else(|| {
"cannot reload: this engine was built with TeraEngine::from_raw, \
which has no template directory to re-read"
.to_string()
})?;
let fresh = tera::Tera::new(pattern)
.map_err(|e| format!("template reload failed, previous templates are still active: {}", e))?;
self.inner = fresh;
Ok(())
}
}
pub fn init(dir: &str) -> Result<(), String> {
let engine = TeraEngine::from_dir(dir)?;
ENGINE
.set(RwLock::new(engine))
.map_err(|_| "template engine already initialized".to_string())
}
pub fn init_from_env() -> Result<(), String> {
let dir = std::env::var(crate::entry_point::Config::RWS_CONFIG_TEMPLATE_DIR)
.unwrap_or_else(|_| {
crate::entry_point::Config::RWS_CONFIG_TEMPLATE_DIR_DEFAULT_VALUE.to_string()
});
init(&dir)
}
pub fn global() -> std::sync::RwLockReadGuard<'static, TeraEngine> {
ENGINE
.get()
.expect("template engine not initialized — call template::init() at startup")
.read()
.unwrap_or_else(|e| e.into_inner())
}
pub fn render(template_name: &str, ctx: &Context) -> Result<Response, String> {
global().response(template_name, ctx)
}
pub fn reload() -> Result<(), String> {
let lock = ENGINE
.get()
.ok_or_else(|| "template engine not initialized — call template::init() at startup".to_string())?;
let mut guard = lock.write().unwrap_or_else(|e| e.into_inner());
guard.reload()
}
pub(crate) fn reload_if_initialized() {
if let Some(lock) = ENGINE.get() {
let mut guard = lock.write().unwrap_or_else(|e| e.into_inner());
if let Err(e) = guard.reload() {
eprintln!("[template] hot reload failed: {}", e);
}
}
}
fn html_response(html: String) -> Response {
let mut r = Response::new();
r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
r.content_range_list = vec![Range::get_content_range(
html.into_bytes(),
MimeType::TEXT_HTML.to_string(),
)];
r
}