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> {
64 let base = Path::new(dir);
65 let mut files: Vec<(PathBuf, String)> = Vec::new();
66 if base.exists() {
67 collect(base, base, &mut files)?;
68 }
69
70 let mut app: Vec<(String, String)> = Vec::with_capacity(files.len());
71 for (path, name) in &files {
72 let content = std::fs::read_to_string(path)
73 .with_context(|| format!("reading template {}", path.display()))?;
74 app.push((name.clone(), content));
75 }
76
77 let framework = crate::global::framework_template_snapshot();
78 match build(&framework, &app) {
79 Ok(tera) => Ok(tera),
80 Err(e) if !framework.is_empty() => {
85 doido_core::tracing::warn!(
86 "framework templates failed to load ({e}); using app templates only"
87 );
88 build(&[], &app)
89 }
90 Err(e) => Err(e),
91 }
92}
93
94fn build(framework: &[(String, String)], app: &[(String, String)]) -> Result<tera::Tera> {
98 let mut raw: Vec<(&str, &str)> = Vec::with_capacity(framework.len() + app.len());
99 for (name, content) in framework {
100 if app.iter().any(|(n, _)| n == name) {
101 continue; }
103 raw.push((name.as_str(), content.as_str()));
104 }
105 for (name, content) in app {
106 raw.push((name.as_str(), content.as_str()));
107 }
108
109 let mut tera = tera::Tera::new();
110 tera.add_raw_templates(raw)
111 .map_err(|e| doido_core::anyhow::anyhow!("{e}"))?;
112 Ok(tera)
113}
114
115fn collect(base: &Path, dir: &Path, out: &mut Vec<(PathBuf, String)>) -> Result<()> {
118 for entry in std::fs::read_dir(dir).with_context(|| format!("reading {}", dir.display()))? {
119 let path = entry?.path();
120 if path.is_dir() {
121 collect(base, &path, out)?;
122 } else if path.extension().and_then(|e| e.to_str()) == Some("tera") {
123 let rel = path.strip_prefix(base).unwrap_or(&path);
124 let name = rel
125 .components()
126 .map(|c| c.as_os_str().to_string_lossy())
127 .collect::<Vec<_>>()
128 .join("/");
129 out.push((path.clone(), name));
130 }
131 }
132 Ok(())
133}