use crate::error::{Result, SnapFireError};
use parking_lot::RwLock;
use serde::Serialize;
use std::sync::Arc;
use tera::{Context, Tera};
#[cfg(feature = "devel")]
use crate::core::reload::DevReloader;
pub struct Template {
pub(crate) app_state: TeraWeb,
pub(crate) template_name: String,
pub(crate) context: Context,
}
#[derive(Clone, Debug)]
pub struct TeraWeb {
pub(crate) tera: Arc<RwLock<Tera>>,
pub(crate) global_context: Arc<Context>,
#[cfg(feature = "devel")]
pub(crate) reloader: Arc<DevReloader>,
}
impl TeraWeb {
pub fn builder(templates_glob: &str) -> TeraWebBuilder {
TeraWebBuilder::new(templates_glob)
}
pub(crate) fn render_with_context(&self, tpl: &str, user_context: Context) -> Result<String> {
let tera = self.tera.read();
let mut final_context = (*self.global_context).clone();
final_context.extend(user_context);
let body = tera.render(tpl, &final_context).map_err(SnapFireError::Tera)?;
Ok(body)
}
pub fn render(&self, tpl: &str, context: Context) -> Template {
Template {
app_state: self.clone(),
template_name: tpl.to_string(),
context,
}
}
#[cfg(feature = "devel")]
pub(crate) fn get_reloader_broadcaster(&self) -> tokio::sync::broadcast::Sender<crate::core::reload::ReloadMessage> {
self.reloader.broadcaster.clone()
}
}
pub struct TeraWebBuilder {
templates_glob: String,
globals: Context,
tera_configurator: Option<Box<dyn FnOnce(&mut Tera)>>,
static_paths_to_watch: Vec<String>,
ws_path: String,
auto_inject_script: bool,
}
impl TeraWebBuilder {
pub(crate) fn new(templates_glob: &str) -> Self {
Self {
templates_glob: templates_glob.to_string(),
globals: Context::new(),
tera_configurator: None,
static_paths_to_watch: Vec::new(),
ws_path: "/_snapfire/ws".to_string(),
auto_inject_script: true,
}
}
pub fn add_global<S: Into<String>, T: Serialize>(mut self, key: S, value: T) -> Self {
self.globals.insert(&key.into(), &value);
self
}
pub fn configure_tera<F>(mut self, configurator: F) -> Self
where
F: FnOnce(&mut Tera) + 'static,
{
self.tera_configurator = Some(Box::new(configurator));
self
}
pub fn ws_path(mut self, path: &str) -> Self {
self.ws_path = path.to_string();
self
}
pub fn auto_inject_script(mut self, enabled: bool) -> Self {
self.auto_inject_script = enabled;
self
}
pub fn watch_static(mut self, path: &str) -> Self {
self.static_paths_to_watch.push(path.to_string());
self
}
pub fn build(self) -> Result<TeraWeb> {
let mut tera = Tera::new(&self.templates_glob)?;
if let Some(configurator) = self.tera_configurator {
configurator(&mut tera);
}
let tera = Arc::new(RwLock::new(tera));
Ok(TeraWeb {
#[cfg(feature = "devel")]
reloader: {
let reloader = DevReloader::start(
Arc::clone(&tera),
&self.templates_glob,
self.static_paths_to_watch,
self.ws_path,
self.auto_inject_script,
)?;
Arc::new(reloader)
},
tera, global_context: Arc::new(self.globals),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
async fn setup_test_app(global_key: &str, global_value: &str, template_content: &str) -> TeraWeb {
let temp_dir = tempdir().unwrap();
let template_path = temp_dir.path().join("index.html");
fs::write(&template_path, template_content).unwrap();
let glob_path = temp_dir.path().join("*.html").to_str().unwrap().to_string();
TeraWeb::builder(&glob_path)
.add_global(global_key, global_value)
.build()
.unwrap()
}
#[tokio::test]
async fn test_render_with_global_context() {
let app = setup_test_app("site_name", "SnapFire Test", "Hello, {{ site_name }}!").await;
let user_context = Context::new();
let result = app.render_with_context("index.html", user_context);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "Hello, SnapFire Test!");
}
#[tokio::test]
async fn test_render_with_user_context() {
let app = setup_test_app("site_name", "Global", "Hello, {{ user_name }}!").await;
let mut user_context = Context::new();
user_context.insert("user_name", "Alice");
let result = app.render_with_context("index.html", user_context);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "Hello, Alice!");
}
#[tokio::test]
async fn test_user_context_overrides_global_context() {
let app = setup_test_app("title", "Global Title", "Title: {{ title }}").await;
let mut user_context = Context::new();
user_context.insert("title", "Page Title");
let result = app.render_with_context("index.html", user_context);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "Title: Page Title");
}
#[test]
fn test_bad_glob_behavior() {
let builder = TeraWeb::builder("/invalid/path/that/does/not/exist/*.html");
#[cfg(feature = "devel")]
{
let result = builder.build();
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), SnapFireError::Watcher(_)));
}
#[cfg(not(feature = "devel"))]
{
let app = builder.build().unwrap();
let result = app.render_with_context("non_existent.html", Context::new());
assert!(matches!(result.unwrap_err(), SnapFireError::Tera(_)));
}
}
#[test]
fn test_configure_tera_hook() {
let temp_dir = tempdir().unwrap();
let template_path = temp_dir.path().join("index.html");
fs::write(&template_path, "Hello, {{ name | upcase }}!").unwrap();
let glob_path = temp_dir.path().join("*.html").to_str().unwrap().to_string();
fn upcase_filter(
value: &tera::Value,
_: &std::collections::HashMap<String, tera::Value>,
) -> tera::Result<tera::Value> {
let s = tera::from_value::<String>(value.clone())?;
Ok(tera::to_value(s.to_uppercase()).unwrap())
}
let app = TeraWeb::builder(&glob_path)
.configure_tera(|tera| {
tera.register_filter("upcase", upcase_filter);
})
.build()
.unwrap();
let mut context = Context::new();
context.insert("name", "world");
let result = app.render_with_context("index.html", context);
assert_eq!(result.unwrap(), "Hello, WORLD!");
}
}