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