doido_view/
tera_engine.rs1use crate::engine::TemplateEngine;
2use doido_core::{anyhow::Context as _, Result};
3use std::path::{Path, PathBuf};
4use std::sync::RwLock;
5
6pub struct TeraEngine {
7 tera: RwLock<tera::Tera>,
8 templates_dir: String,
9}
10
11impl TeraEngine {
12 pub fn new(templates_dir: &str) -> Result<Self> {
13 let tera = load(templates_dir)
14 .with_context(|| format!("failed to load templates from {templates_dir}"))?;
15 Ok(Self {
16 tera: RwLock::new(tera),
17 templates_dir: templates_dir.to_string(),
18 })
19 }
20}
21
22impl TemplateEngine for TeraEngine {
23 fn render(&self, template: &str, context: &serde_json::Value) -> Result<String> {
24 let template_name = format!("{}.html.tera", template);
25 let ctx = tera::Context::from_serialize(context)
26 .map_err(|e| doido_core::anyhow::anyhow!("invalid template context: {e}"))?;
27 self.tera
28 .read()
29 .unwrap()
30 .render(&template_name, &ctx)
31 .map_err(|e| doido_core::anyhow::anyhow!("template '{}' render failed: {e}", template))
32 }
33
34 fn render_named(&self, name: &str, context: &serde_json::Value) -> Result<String> {
35 let ctx = tera::Context::from_serialize(context)
36 .map_err(|e| doido_core::anyhow::anyhow!("invalid template context: {e}"))?;
37 self.tera
38 .read()
39 .unwrap()
40 .render(name, &ctx)
41 .map_err(|e| doido_core::anyhow::anyhow!("template '{}' render failed: {e}", name))
42 }
43
44 fn reload(&self) -> Result<()> {
45 let tera = load(&self.templates_dir)
46 .with_context(|| format!("reload failed for {}", self.templates_dir))?;
47 *self.tera.write().unwrap() = tera;
48 Ok(())
49 }
50}
51
52fn load(dir: &str) -> Result<tera::Tera> {
58 let base = Path::new(dir);
59 let mut files: Vec<(PathBuf, String)> = Vec::new();
60 if base.exists() {
61 collect(base, base, &mut files)?;
62 }
63 let mut tera = tera::Tera::new();
64 tera.add_template_files(files.iter().map(|(path, name)| (path, Some(name.as_str()))))
65 .map_err(|e| doido_core::anyhow::anyhow!("{e}"))?;
66 Ok(tera)
67}
68
69fn collect(base: &Path, dir: &Path, out: &mut Vec<(PathBuf, String)>) -> Result<()> {
72 for entry in std::fs::read_dir(dir).with_context(|| format!("reading {}", dir.display()))? {
73 let path = entry?.path();
74 if path.is_dir() {
75 collect(base, &path, out)?;
76 } else if path.extension().and_then(|e| e.to_str()) == Some("tera") {
77 let rel = path.strip_prefix(base).unwrap_or(&path);
78 let name = rel
79 .components()
80 .map(|c| c.as_os_str().to_string_lossy())
81 .collect::<Vec<_>>()
82 .join("/");
83 out.push((path.clone(), name));
84 }
85 }
86 Ok(())
87}