1use crate::{Error, Result};
2use std::path::{Path, PathBuf};
3
4pub trait TemplateSource: Send + Sync {
10 fn load(&self, name: &str, locale: &str, default_locale: &str) -> Result<String>;
20}
21
22pub struct FileSource {
30 path: PathBuf,
31}
32
33impl FileSource {
34 pub fn new(templates_path: impl Into<PathBuf>) -> Self {
36 Self {
37 path: templates_path.into(),
38 }
39 }
40
41 fn try_load(&self, file_path: &Path) -> Option<String> {
42 std::fs::read_to_string(file_path).ok()
43 }
44}
45
46impl TemplateSource for FileSource {
47 fn load(&self, name: &str, locale: &str, default_locale: &str) -> Result<String> {
48 let filename = format!("{name}.md");
49
50 let path = self.path.join(locale).join(&filename);
52 if let Some(content) = self.try_load(&path) {
53 return Ok(content);
54 }
55
56 if locale != default_locale {
58 let path = self.path.join(default_locale).join(&filename);
59 if let Some(content) = self.try_load(&path) {
60 return Ok(content);
61 }
62 }
63
64 let path = self.path.join(&filename);
66 if let Some(content) = self.try_load(&path) {
67 return Ok(content);
68 }
69
70 Err(Error::not_found(format!(
72 "email template '{name}' not found for locale '{locale}'"
73 )))
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 fn setup_templates(dir: &std::path::Path) {
82 std::fs::create_dir_all(dir.join("en")).unwrap();
84 std::fs::write(
85 dir.join("en/welcome.md"),
86 "---\nsubject: Welcome EN\n---\nEnglish body",
87 )
88 .unwrap();
89
90 std::fs::create_dir_all(dir.join("uk")).unwrap();
92 std::fs::write(
93 dir.join("uk/welcome.md"),
94 "---\nsubject: Welcome UK\n---\nUkrainian body",
95 )
96 .unwrap();
97
98 std::fs::write(
100 dir.join("fallback.md"),
101 "---\nsubject: Fallback\n---\nFallback body",
102 )
103 .unwrap();
104 }
105
106 #[test]
107 fn load_exact_locale() {
108 let dir = tempfile::tempdir().unwrap();
109 setup_templates(dir.path());
110 let source = FileSource::new(dir.path());
111
112 let content = source.load("welcome", "uk", "en").unwrap();
113 assert!(content.contains("Ukrainian body"));
114 }
115
116 #[test]
117 fn load_falls_back_to_default_locale() {
118 let dir = tempfile::tempdir().unwrap();
119 setup_templates(dir.path());
120 let source = FileSource::new(dir.path());
121
122 let content = source.load("welcome", "fr", "en").unwrap();
123 assert!(content.contains("English body"));
124 }
125
126 #[test]
127 fn load_falls_back_to_no_locale() {
128 let dir = tempfile::tempdir().unwrap();
129 setup_templates(dir.path());
130 let source = FileSource::new(dir.path());
131
132 let content = source.load("fallback", "fr", "en").unwrap();
133 assert!(content.contains("Fallback body"));
134 }
135
136 #[test]
137 fn load_not_found() {
138 let dir = tempfile::tempdir().unwrap();
139 setup_templates(dir.path());
140 let source = FileSource::new(dir.path());
141
142 let result = source.load("nonexistent", "en", "en");
143 assert!(result.is_err());
144 }
145
146 #[test]
147 fn load_same_locale_as_default_skips_duplicate() {
148 let dir = tempfile::tempdir().unwrap();
149 setup_templates(dir.path());
150 let source = FileSource::new(dir.path());
151
152 let content = source.load("welcome", "en", "en").unwrap();
154 assert!(content.contains("English body"));
155 }
156}