Skip to main content

sofie_mail/
templates.rs

1use std::sync::Arc;
2
3use sofie_core::{Error, Result, Show};
4use sofie_db::{Db, File};
5
6use crate::Body;
7use crate::language::{Language, rendered};
8
9/// A template the build script compiled, keyed `<crate>/<name>` after its file `mails/<name>.html`.
10#[derive(Clone, Copy, Debug)]
11pub struct Template {
12    key: &'static str,
13    file: File,
14}
15
16impl Template {
17    #[must_use]
18    pub const fn new(key: &'static str, file: File) -> Self {
19        Self { key, file }
20    }
21
22    #[must_use]
23    pub fn key(&self) -> &'static str {
24        self.key
25    }
26
27    /// Loaded per mail: mails are few, and a template is a short read of bytes.
28    pub(crate) fn render(
29        &self,
30        data: &(impl Show + ?Sized),
31        language: Language<'_>,
32    ) -> Result<Body> {
33        let template = self.load()?;
34        let html = rendered(&template, data, language).map_err(|error| self.failed(error))?;
35        Ok(Body::rendered(html))
36    }
37
38    #[cfg(not(feature = "development"))]
39    fn load(&self) -> Result<sofie_engine::Template> {
40        sofie_engine::load_template(self.key, self.file.bytes).map_err(|error| self.failed(error))
41    }
42
43    #[cfg(feature = "development")]
44    fn load(&self) -> Result<sofie_engine::Template> {
45        self.parsed_from_disk()
46    }
47
48    #[cfg(any(test, feature = "development"))]
49    fn parsed_from_disk(&self) -> Result<sofie_engine::Template> {
50        let source = std::fs::read_to_string(self.file.path).map_err(|error| {
51            Error::internal(
52                format!("reading mail `{}` from {}", self.key, self.file.path),
53                error,
54            )
55        })?;
56        sofie_engine::parse_template_with_recorded(
57            self.key,
58            &source,
59            &sofie_engine::Components::default(),
60            self.file.bytes,
61        )
62        .map_err(|error| self.failed(error))
63    }
64
65    fn failed(&self, error: sofie_engine::TemplateError) -> Error {
66        Error::internal(
67            format!("mail `{}` from {}", self.key, self.file.path),
68            error,
69        )
70    }
71}
72
73pub(crate) struct Registered(pub(crate) &'static [Template]);
74
75pub fn templates(db: &Db, templates: &'static [Template]) {
76    sofie_db::extend(db, Arc::new(Registered(templates)));
77}
78
79pub(crate) fn find<'a>(registered: &'a [Arc<Registered>], key: &str) -> Option<&'a Template> {
80    registered
81        .iter()
82        .flat_map(|table| table.0)
83        .find(|template| template.key == key)
84}
85
86#[cfg(test)]
87mod tests {
88    use serde_json::json;
89
90    use super::*;
91
92    #[test]
93    fn a_development_build_reads_the_template_as_it_is_on_disk() {
94        let path = std::env::temp_dir().join(format!("sofie-mail-{}.html", std::process::id()));
95        std::fs::write(&path, "<p>old {data.name}</p>").unwrap();
96        let path: &'static str = String::leak(path.to_str().unwrap().to_owned());
97        let template = Template::new(
98            "order/receipt",
99            File::new(
100                path,
101                Vec::leak(sofie_engine::compile_template("built", "").unwrap()),
102            ),
103        );
104        let html = |template: &Template| {
105            template
106                .parsed_from_disk()
107                .unwrap()
108                .render(&json!({"name": "Ada"}))
109                .unwrap()
110        };
111        assert_eq!(html(&template), "<p>old Ada</p>");
112
113        std::fs::write(path, "<p>new {data.name}</p>").unwrap();
114
115        assert_eq!(html(&template), "<p>new Ada</p>");
116        std::fs::remove_file(path).unwrap();
117    }
118}