Skip to main content

doido_view/
global.rs

1//! Process-global template engine, installed once at boot and reached from
2//! request handlers via `Context::render`.
3//!
4//! Mirrors the framework's other boot-time singletons (the DB pool, inflections):
5//! the application installs an engine with [`init`]/[`set_engine`] and controllers
6//! render through [`render`] without threading the engine through every call.
7
8use crate::engine::TemplateEngine;
9use crate::tera_engine::TeraEngine;
10use doido_core::Result;
11use std::sync::{Arc, OnceLock};
12
13static ENGINE: OnceLock<Arc<dyn TemplateEngine>> = OnceLock::new();
14
15/// Installs a template engine globally. Idempotent: a second call is ignored.
16pub fn set_engine(engine: Arc<dyn TemplateEngine>) {
17    let _ = ENGINE.set(engine);
18}
19
20/// Installs the default [`TeraEngine`] over `templates_dir` (e.g. `app/views`),
21/// loading every `**/*.tera` file under it. Idempotent. Call once at boot.
22pub fn init(templates_dir: &str) -> Result<()> {
23    if ENGINE.get().is_some() {
24        return Ok(());
25    }
26    let engine = TeraEngine::new(templates_dir)?;
27    set_engine(Arc::new(engine));
28    Ok(())
29}
30
31/// Returns the installed engine, if any.
32pub fn try_engine() -> Option<Arc<dyn TemplateEngine>> {
33    ENGINE.get().cloned()
34}
35
36/// Renders `template` (without the `.html.tera` suffix) with `context` to an
37/// HTML string. Errors if no engine was installed or the template fails.
38pub fn render(template: &str, context: &serde_json::Value) -> Result<String> {
39    let engine = ENGINE.get().ok_or_else(|| {
40        doido_core::anyhow::anyhow!(
41            "view engine not initialised; call doido_view::init(\"app/views\") at boot"
42        )
43    })?;
44    engine.render(template, context)
45}
46
47/// Render a specific format `variant` of `template` (e.g. `("mailers/x/welcome",
48/// "text")` renders `mailers/x/welcome.text.tera`). Used for mailer html/text
49/// parts, where `render` — which always appends `.html.tera` — is not enough.
50pub fn render_variant(
51    template: &str,
52    variant: &str,
53    context: &serde_json::Value,
54) -> Result<String> {
55    let engine = ENGINE.get().ok_or_else(|| {
56        doido_core::anyhow::anyhow!(
57            "view engine not initialised; call doido_view::init(\"app/views\") at boot"
58        )
59    })?;
60    engine.render_named(&format!("{template}.{variant}.tera"), context)
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use crate::engine::TemplateEngine;
67
68    struct StubEngine;
69    impl TemplateEngine for StubEngine {
70        fn render(&self, template: &str, _ctx: &serde_json::Value) -> Result<String> {
71            Ok(format!("stub:{template}"))
72        }
73        fn reload(&self) -> Result<()> {
74            Ok(())
75        }
76    }
77
78    #[test]
79    fn set_then_render_uses_installed_engine() {
80        set_engine(Arc::new(StubEngine));
81        assert_eq!(
82            render("posts/index", &serde_json::json!({})).unwrap(),
83            "stub:posts/index"
84        );
85    }
86}