1use std::sync::Arc;
2
3use sofie_core::{Error, Result, Show};
4use sofie_db::{Db, File};
5
6use crate::Body;
7
8#[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 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}