#[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 mut tera = tera::Tera::new(pattern)
.map_err(|e| format!("template engine init failed: {}", e))?;
register_helpers(&mut tera);
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();
register_helpers(&mut tera);
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 has_template(&self, name: &str) -> bool {
self.inner.get_template_names().any(|n| n == name)
}
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 mut fresh = tera::Tera::new(pattern)
.map_err(|e| format!("template reload failed, previous templates are still active: {}", e))?;
register_helpers(&mut fresh);
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())?;
crate::config_reload::on_reload(reload_if_initialized);
Ok(())
}
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 try_global() -> Option<std::sync::RwLockReadGuard<'static, TeraEngine>> {
ENGINE.get().map(|lock| lock.read().unwrap_or_else(|e| e.into_inner()))
}
pub fn render(template_name: &str, ctx: &Context) -> Result<Response, String> {
if reloads_on_render() {
reload_if_initialized();
}
global().response(template_name, ctx)
}
fn reloads_on_render() -> bool {
static RELOAD: OnceLock<bool> = OnceLock::new();
*RELOAD.get_or_init(|| crate::entry_point::RwsEnv::current().reloads_templates())
}
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 arg_text(value: &Value) -> String {
match value {
Value::String(s) => s.clone(),
other => other.to_string(),
}
}
fn register_helpers(tera: &mut tera::Tera) {
tera.register_function("url_for", |args: &std::collections::HashMap<String, Value>| {
let name = args.get("name").and_then(Value::as_str).ok_or_else(|| tera::Error::msg("url_for: needs name=\"<route name>\""))?;
let mut params: Vec<(String, String)> = args.iter().filter(|(k, _)| k.as_str() != "name").map(|(k, v)| (k.clone(), arg_text(v))).collect();
params.sort();
let borrowed: Vec<(&str, &str)> = params.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
crate::router::url_for(name, &borrowed)
.map(Value::String)
.ok_or_else(|| tera::Error::msg(format!("url_for: no route named {name:?}, or a parameter of it is missing")))
});
tera.register_function("vite", |args: &std::collections::HashMap<String, Value>| {
let entry = args.get("entry").and_then(Value::as_str).ok_or_else(|| tera::Error::msg("vite: needs entry=\"<source path>\""))?;
crate::vite::tags(entry)
.map(Value::String)
.ok_or_else(|| tera::Error::msg(format!("vite: no entry {entry:?} (or no Vite helper installed: rust_web_server::vite::install)")))
});
tera.register_function("vite_asset", |args: &std::collections::HashMap<String, Value>| {
let path = args.get("path").and_then(Value::as_str).ok_or_else(|| tera::Error::msg("vite_asset: needs path=\"<source path>\""))?;
crate::vite::asset(path).map(Value::String).ok_or_else(|| tera::Error::msg(format!("vite_asset: no asset {path:?}")))
});
}
pub fn page_context(request: &crate::request::Request) -> Context {
let mut ctx = Context::new();
let flash = crate::flash::Flash::read(request);
let messages: Vec<Value> = flash
.messages()
.iter()
.map(|m| serde_json::json!({ "kind": m.kind, "text": m.text }))
.collect();
ctx.insert("flash", &messages);
if let Some(notice) = flash.notice() {
ctx.insert("notice", notice);
}
if let Some(alert) = flash.alert() {
ctx.insert("alert", alert);
}
let token = csrf_token(request);
let field = if token.is_empty() {
String::new()
} else {
format!(r#"<input type="hidden" name="_csrf" value="{}">"#, token.replace('&', "&").replace('"', """).replace('<', "<"))
};
ctx.insert("csrf_token", &token);
ctx.insert("csrf_field", &field);
ctx.insert("path", request.request_uri.split('?').next().unwrap_or("/"));
ctx
}
#[cfg(feature = "csrf")]
fn csrf_token(request: &crate::request::Request) -> String {
if let Some(token) = crate::csrf::CsrfToken::from_request(request) {
return token.value().to_string();
}
request
.header("cookie")
.and_then(|h| crate::cookie::CookieJar::parse(&h.value).get("_csrf").map(|c| c.value.clone()))
.unwrap_or_default()
}
#[cfg(not(feature = "csrf"))]
fn csrf_token(_request: &crate::request::Request) -> String {
String::new()
}
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
}