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#[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 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}